@@ -51,9 +51,19 @@ export const getKey = (e: ItemElement, i: number): React.Key => {
5151 return key != null ? key : "_" + i ;
5252} ;
5353
54+ const clamp = ( value : number , minValue : number , maxValue : number ) : number =>
55+ Math . min ( maxValue , Math . max ( minValue , value ) ) ;
56+
57+ /**
58+ * A cache implementation that stores values indexed by number, with optional LRU (Least Recently Used) eviction.
59+ *
60+ * - Uses a sparse array for efficient range cleanup and memory usage.
61+ * - Optionally limits the number of cached items using a numerical LRU queue.
62+ * - Provides methods to get, set, and clear cached values by index or range.
63+ */
5464export class IndexedCache < T > {
5565 #cache: T [ ] ;
56- #cachedIndicesOrder: CircularUniqueQueue < number > | undefined ;
66+ #cachedIndicesOrder: NumericalLruQueue | undefined ;
5767
5868 constructor ( length : number , cacheLimit : number | undefined | null ) {
5969 // Using a sparse array instead of a Map enables faster cleanup of a specific
@@ -62,11 +72,11 @@ export class IndexedCache<T> {
6272 // benefits of both an array and a hash map.
6373 this . #cache = new Array < T > ( length ) ;
6474
65- // Using a circular queue for indices order enables deletion of the oldest
75+ // Using a LRU queue for indices order enables deletion of the oldest
6676 // value when threshold is reached, to not exceed the limit, but yet keep
6777 // the most recent values in the cache.
6878 if ( cacheLimit && cacheLimit < length ) {
69- this . #cachedIndicesOrder = new CircularUniqueQueue < number > ( cacheLimit ) ;
79+ this . #cachedIndicesOrder = new NumericalLruQueue ( length , cacheLimit ) ;
7080 }
7181 }
7282
@@ -77,7 +87,7 @@ export class IndexedCache<T> {
7787 set ( i : number , value : T ) {
7888 if ( this . #cachedIndicesOrder && this . #cache[ i ] === undefined ) {
7989 const cacheIndexToRemove = this . #cachedIndicesOrder. enqueue ( i ) ;
80- if ( cacheIndexToRemove !== undefined ) {
90+ if ( cacheIndexToRemove !== null ) {
8191 delete this . #cache[ cacheIndexToRemove ] ;
8292 }
8393 }
@@ -87,74 +97,145 @@ export class IndexedCache<T> {
8797
8898 clearRange (
8999 startIndex : number | undefined = 0 ,
90- endIndex : number | undefined = this . #cache. length
100+ endIndex : number | undefined = this . #cache. length - 1
91101 ) {
92- const deletedIndices = this . #cachedIndicesOrder && new Set < number > ( ) ;
93-
94- for ( const indexStr in this . #cache) {
95- const index = Number ( indexStr ) ;
96- if ( index >= startIndex || index < endIndex ) {
97- delete this . #cache[ index ] ;
102+ startIndex = clamp ( startIndex , 0 , this . #cache. length - 1 ) ;
103+ endIndex = clamp ( endIndex , 0 , this . #cache. length - 1 ) ;
98104
99- if ( deletedIndices ) {
100- deletedIndices . add ( index ) ;
101- }
102- }
103-
104- if ( index >= endIndex ) {
105- break ;
106- }
105+ for ( let i = startIndex ; i <= endIndex ; i ++ ) {
106+ delete this . #cache[ i ] ;
107107 }
108108
109- if ( deletedIndices && this . #cachedIndicesOrder) {
110- this . #cachedIndicesOrder. filter ( ( i ) => deletedIndices . has ( i ) ) ;
111- }
109+ this . #cachedIndicesOrder?. clearRange ( startIndex , endIndex ) ;
112110 }
113111}
114112
115- class CircularUniqueQueue < T > {
116- #data: ( T | undefined ) [ ] ;
117- #head = 0 ;
118- #tail = 0 ;
119- #size = 0 ;
120- #capacity: number ;
121- #set: Set < T > ;
113+ /**
114+ * A numerical Least Recently Used (LRU) queue optimized for integer keys in the range [0, maxValue).
115+ *
116+ * This class maintains a fixed-capacity LRU queue using efficient array-based doubly-linked lists,
117+ * allowing for fast O(1) operations for insertion, access, and removal. It is designed for scenarios
118+ * where the set of possible keys is known and bounded, such as managing resources or cache entries
119+ * indexed by small integers.
120+ *
121+ * @example
122+ * ```typescript
123+ * const lru = new NumericalLruQueue(100, 10); // maxValue = 100, capacity = 10
124+ * lru.enqueue(7); // Access or insert 7, evict if over capacity
125+ * lru.dequeue(); // Remove and return the least recently used element
126+ * lru.clearRange(0, 9); // Remove all elements in the range [0, 9]
127+ * ```
128+ *
129+ * @remarks
130+ * - Keys must be integers in the range [0, maxValue).
131+ * - Not thread-safe.
132+ *
133+ * @public
134+ */
135+ class NumericalLruQueue {
136+ readonly #capacity: number ;
137+ readonly #maxValue: number ;
138+
139+ // Doubly-linked list arrays (store indices into 0..maxValue-1)
140+ #next: Int32Array ; // length = maxValue
141+ #prev: Int32Array ; // length = maxValue
142+
143+ // Tracks whether element is in the LRU (and if so, points to itself)
144+ // -1 means "not present"
145+ #position: Int32Array ; // length = maxValue
146+
147+ #head: number ; // LRU oldest
148+ #tail: number ; // LRU newest
149+ #size: number ;
122150
123- constructor ( capacity : number ) {
124- this . #data = new Array < T > ( capacity ) ;
151+ constructor ( maxValue : number , capacity : number ) {
152+ this . #maxValue = maxValue ;
125153 this . #capacity = capacity ;
126- this . #set = new Set < T > ( ) ;
127- }
128154
129- enqueue ( item : T ) {
130- if ( this . #set. has ( item ) ) return undefined ;
131- const dequeuedItem =
132- this . #size === this . #capacity ? this . dequeue ( ) : undefined ;
155+ this . #next = new Int32Array ( maxValue ) ;
156+ this . #prev = new Int32Array ( maxValue ) ;
157+ this . #position = new Int32Array ( maxValue ) ;
133158
134- this . #data [ this . #tail ] = item ;
135- this . #set . add ( item ) ;
136- this . #tail = ( this . #tail + 1 ) % this . #capacity ;
137- this . #size ++ ;
138- return dequeuedItem ;
159+ this . #position . fill ( - 1 ) ;
160+
161+ this . #head = - 1 ;
162+ this . #tail = - 1 ;
163+ this . #size = 0 ;
139164 }
140165
141- dequeue ( ) {
142- if ( this . #size === 0 ) return undefined ;
143- const item = this . #data[ this . #head] ;
144- if ( item !== undefined ) this . #set. delete ( item ) ;
166+ /**
167+ * Adds the item at the specified index to the queue and manages the queue size.
168+ * If adding the item causes the queue to exceed its capacity, the oldest item is dequeued and its value is returned.
169+ * Otherwise, returns `null`.
170+ */
171+ enqueue ( i : number ) : number | null {
172+ if ( i < 0 || i >= this . #maxValue) return null ;
173+
174+ this . #access( i ) ;
175+ if ( this . #size > this . #capacity) {
176+ return this . dequeue ( ) ;
177+ }
178+ return null ;
179+ }
145180
146- this . #data[ this . #head] = undefined ;
147- this . #head = ( this . #head + 1 ) % this . #capacity;
148- this . #size-- ;
149- return item ;
181+ /** Evicts the least used item. If the queue is empty, returns null. */
182+ dequeue ( ) : number | null {
183+ if ( this . #head === - 1 ) return null ;
184+ const victim = this . #head;
185+ this . #detach( victim ) ;
186+ return victim ;
150187 }
151188
152- filter ( predicate : ( item : T ) => boolean ) {
153- for ( let i = 0 ; i < this . #size; i ++ ) {
154- const item = this . dequeue ( ) ;
155- if ( item !== undefined && predicate ( item ) ) {
156- this . enqueue ( item ) ;
189+ /** Efficiently finds and detaches all items within the desired range. */
190+ clearRange ( startIndex : number , endIndex : number ) : void {
191+ startIndex = clamp ( startIndex , 0 , this . #maxValue - 1 ) ;
192+ endIndex = clamp ( endIndex , 0 , this . #maxValue - 1 ) ;
193+
194+ for ( let i = startIndex ; i <= endIndex ; i ++ ) {
195+ if ( this . #position[ i ] !== - 1 ) {
196+ this . #detach( i ) ;
157197 }
158198 }
159199 }
200+
201+ #access( i : number ) : void {
202+ if ( this . #position[ i ] === - 1 ) {
203+ this . #insertAtTail( i ) ;
204+ } else {
205+ this . #detach( i ) ;
206+ this . #append( i ) ;
207+ }
208+ }
209+
210+ #insertAtTail( i : number ) : void {
211+ this . #append( i ) ;
212+ this . #size++ ;
213+ this . #position[ i ] = i ;
214+ }
215+
216+ #append( i : number ) : void {
217+ this . #prev[ i ] = this . #tail;
218+ this . #next[ i ] = - 1 ;
219+ if ( this . #tail !== - 1 ) this . #next[ this . #tail] = i ;
220+ this . #tail = i ;
221+ if ( this . #head === - 1 ) this . #head = i ;
222+ this . #position[ i ] = i ;
223+ }
224+
225+ #detach( i : number ) : void {
226+ const p = this . #prev[ i ] ! ;
227+ const n = this . #next[ i ] ! ;
228+
229+ if ( p !== - 1 ) this . #next[ p ] = n ;
230+ if ( n !== - 1 ) this . #prev[ n ] = p ;
231+
232+ if ( this . #head === i ) this . #head = n ;
233+ if ( this . #tail === i ) this . #tail = p ;
234+
235+ this . #prev[ i ] = - 1 ;
236+ this . #next[ i ] = - 1 ;
237+ this . #position[ i ] = - 1 ;
238+
239+ this . #size-- ;
240+ }
160241}
0 commit comments