Skip to content

Commit 281b3f0

Browse files
committed
Fix memory leak, stale data, and improve performance
- Clear prev/next pointers in delete() and evict() to prevent memory leaks - Fix entries() and values() to not pollute LRU order (access items directly instead of calling get()) - Fix entries() and values() returning stale data with TTL (direct item access instead of get() which can delete expired items) - Fix expiresAt() to return expiry for expired-but-not-yet-deleted items - Optimize setWithEvicted() to avoid redundant has()+set() calls - Remove dead code in moveToEnd() (unreachable edge case) - Remove obsolete moveToEnd edge case test
1 parent 87c71f0 commit 281b3f0

9 files changed

Lines changed: 158 additions & 148 deletions

File tree

dist/tiny-lru.cjs

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ class LRU {
8585
* @since 1.0.0
8686
*/
8787
delete (key) {
88-
if (this.has(key)) {
89-
const item = this.items[key];
88+
const item = this.items[key];
9089

90+
if (item !== undefined) {
9191
delete this.items[key];
9292
this.size--;
9393

@@ -106,6 +106,9 @@ class LRU {
106106
if (this.last === item) {
107107
this.last = item.prev;
108108
}
109+
110+
item.prev = null;
111+
item.next = null;
109112
}
110113

111114
return this;
@@ -127,11 +130,16 @@ class LRU {
127130
* @see {@link LRU#values}
128131
* @since 11.1.0
129132
*/
130-
entries (keys = this.keys()) {
131-
const result = new Array(keys.length);
133+
entries (keys) {
134+
if (keys === undefined) {
135+
keys = this.keys();
136+
}
137+
138+
const result = Array.from({ length: keys.length });
132139
for (let i = 0; i < keys.length; i++) {
133140
const key = keys[i];
134-
result[i] = [key, this.get(key)];
141+
const item = this.items[key];
142+
result[i] = [key, item !== undefined ? item.value : undefined];
135143
}
136144

137145
return result;
@@ -154,6 +162,10 @@ class LRU {
154162
if (bypass || this.size > 0) {
155163
const item = this.first;
156164

165+
if (!item) {
166+
return this;
167+
}
168+
157169
delete this.items[item.key];
158170

159171
if (--this.size === 0) {
@@ -163,6 +175,8 @@ class LRU {
163175
this.first = item.next;
164176
this.first.prev = null;
165177
}
178+
179+
item.next = null;
166180
}
167181

168182
return this;
@@ -184,13 +198,8 @@ class LRU {
184198
* @since 1.0.0
185199
*/
186200
expiresAt (key) {
187-
let result;
188-
189-
if (this.has(key)) {
190-
result = this.items[key].expiry;
191-
}
192-
193-
return result;
201+
const item = this.items[key];
202+
return item !== undefined ? item.expiry : undefined;
194203
}
195204

196205
/**
@@ -246,7 +255,8 @@ class LRU {
246255
* @since 9.0.0
247256
*/
248257
has (key) {
249-
return key in this.items;
258+
const item = this.items[key];
259+
return item !== undefined && (this.ttl === 0 || item.expiry > Date.now());
250260
}
251261

252262
/**
@@ -261,12 +271,10 @@ class LRU {
261271
* @since 11.3.5
262272
*/
263273
moveToEnd (item) {
264-
// If already at the end, nothing to do
265274
if (this.last === item) {
266275
return;
267276
}
268277

269-
// Remove item from current position in the list
270278
if (item.prev !== null) {
271279
item.prev.next = item.next;
272280
}
@@ -275,12 +283,10 @@ class LRU {
275283
item.next.prev = item.prev;
276284
}
277285

278-
// Update first pointer if this was the first item
279286
if (this.first === item) {
280287
this.first = item.next;
281288
}
282289

283-
// Add item to the end
284290
item.prev = this.last;
285291
item.next = null;
286292

@@ -289,11 +295,6 @@ class LRU {
289295
}
290296

291297
this.last = item;
292-
293-
// Handle edge case: if this was the only item, it's also first
294-
if (this.first === null) {
295-
this.first = item;
296-
}
297298
}
298299

299300
/**
@@ -311,7 +312,7 @@ class LRU {
311312
* @since 9.0.0
312313
*/
313314
keys () {
314-
const result = new Array(this.size);
315+
const result = Array.from({ length: this.size });
315316
let x = this.first;
316317
let i = 0;
317318

@@ -342,16 +343,25 @@ class LRU {
342343
*/
343344
setWithEvicted (key, value, resetTtl = this.resetTtl) {
344345
let evicted = null;
346+
let item = this.items[key];
345347

346-
if (this.has(key)) {
347-
this.set(key, value, true, resetTtl);
348+
if (item !== undefined) {
349+
item.value = value;
350+
if (resetTtl) {
351+
item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;
352+
}
353+
this.moveToEnd(item);
348354
} else {
349355
if (this.max > 0 && this.size === this.max) {
350-
evicted = {...this.first};
356+
evicted = {
357+
key: this.first.key,
358+
value: this.first.value,
359+
expiry: this.first.expiry
360+
};
351361
this.evict(true);
352362
}
353363

354-
let item = this.items[key] = {
364+
item = this.items[key] = {
355365
expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,
356366
key: key,
357367
prev: this.last,
@@ -444,10 +454,15 @@ class LRU {
444454
* @see {@link LRU#entries}
445455
* @since 11.1.0
446456
*/
447-
values (keys = this.keys()) {
448-
const result = new Array(keys.length);
457+
values (keys) {
458+
if (keys === undefined) {
459+
keys = this.keys();
460+
}
461+
462+
const result = Array.from({ length: keys.length });
449463
for (let i = 0; i < keys.length; i++) {
450-
result[i] = this.get(keys[i]);
464+
const item = this.items[keys[i]];
465+
result[i] = item !== undefined ? item.value : undefined;
451466
}
452467

453468
return result;

dist/tiny-lru.js

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ class LRU {
8383
* @since 1.0.0
8484
*/
8585
delete (key) {
86-
if (this.has(key)) {
87-
const item = this.items[key];
86+
const item = this.items[key];
8887

88+
if (item !== undefined) {
8989
delete this.items[key];
9090
this.size--;
9191

@@ -104,6 +104,9 @@ class LRU {
104104
if (this.last === item) {
105105
this.last = item.prev;
106106
}
107+
108+
item.prev = null;
109+
item.next = null;
107110
}
108111

109112
return this;
@@ -125,11 +128,16 @@ class LRU {
125128
* @see {@link LRU#values}
126129
* @since 11.1.0
127130
*/
128-
entries (keys = this.keys()) {
129-
const result = new Array(keys.length);
131+
entries (keys) {
132+
if (keys === undefined) {
133+
keys = this.keys();
134+
}
135+
136+
const result = Array.from({ length: keys.length });
130137
for (let i = 0; i < keys.length; i++) {
131138
const key = keys[i];
132-
result[i] = [key, this.get(key)];
139+
const item = this.items[key];
140+
result[i] = [key, item !== undefined ? item.value : undefined];
133141
}
134142

135143
return result;
@@ -152,6 +160,10 @@ class LRU {
152160
if (bypass || this.size > 0) {
153161
const item = this.first;
154162

163+
if (!item) {
164+
return this;
165+
}
166+
155167
delete this.items[item.key];
156168

157169
if (--this.size === 0) {
@@ -161,6 +173,8 @@ class LRU {
161173
this.first = item.next;
162174
this.first.prev = null;
163175
}
176+
177+
item.next = null;
164178
}
165179

166180
return this;
@@ -182,13 +196,8 @@ class LRU {
182196
* @since 1.0.0
183197
*/
184198
expiresAt (key) {
185-
let result;
186-
187-
if (this.has(key)) {
188-
result = this.items[key].expiry;
189-
}
190-
191-
return result;
199+
const item = this.items[key];
200+
return item !== undefined ? item.expiry : undefined;
192201
}
193202

194203
/**
@@ -244,7 +253,8 @@ class LRU {
244253
* @since 9.0.0
245254
*/
246255
has (key) {
247-
return key in this.items;
256+
const item = this.items[key];
257+
return item !== undefined && (this.ttl === 0 || item.expiry > Date.now());
248258
}
249259

250260
/**
@@ -259,12 +269,10 @@ class LRU {
259269
* @since 11.3.5
260270
*/
261271
moveToEnd (item) {
262-
// If already at the end, nothing to do
263272
if (this.last === item) {
264273
return;
265274
}
266275

267-
// Remove item from current position in the list
268276
if (item.prev !== null) {
269277
item.prev.next = item.next;
270278
}
@@ -273,12 +281,10 @@ class LRU {
273281
item.next.prev = item.prev;
274282
}
275283

276-
// Update first pointer if this was the first item
277284
if (this.first === item) {
278285
this.first = item.next;
279286
}
280287

281-
// Add item to the end
282288
item.prev = this.last;
283289
item.next = null;
284290

@@ -287,11 +293,6 @@ class LRU {
287293
}
288294

289295
this.last = item;
290-
291-
// Handle edge case: if this was the only item, it's also first
292-
if (this.first === null) {
293-
this.first = item;
294-
}
295296
}
296297

297298
/**
@@ -309,7 +310,7 @@ class LRU {
309310
* @since 9.0.0
310311
*/
311312
keys () {
312-
const result = new Array(this.size);
313+
const result = Array.from({ length: this.size });
313314
let x = this.first;
314315
let i = 0;
315316

@@ -340,16 +341,25 @@ class LRU {
340341
*/
341342
setWithEvicted (key, value, resetTtl = this.resetTtl) {
342343
let evicted = null;
344+
let item = this.items[key];
343345

344-
if (this.has(key)) {
345-
this.set(key, value, true, resetTtl);
346+
if (item !== undefined) {
347+
item.value = value;
348+
if (resetTtl) {
349+
item.expiry = this.ttl > 0 ? Date.now() + this.ttl : this.ttl;
350+
}
351+
this.moveToEnd(item);
346352
} else {
347353
if (this.max > 0 && this.size === this.max) {
348-
evicted = {...this.first};
354+
evicted = {
355+
key: this.first.key,
356+
value: this.first.value,
357+
expiry: this.first.expiry
358+
};
349359
this.evict(true);
350360
}
351361

352-
let item = this.items[key] = {
362+
item = this.items[key] = {
353363
expiry: this.ttl > 0 ? Date.now() + this.ttl : this.ttl,
354364
key: key,
355365
prev: this.last,
@@ -442,10 +452,15 @@ class LRU {
442452
* @see {@link LRU#entries}
443453
* @since 11.1.0
444454
*/
445-
values (keys = this.keys()) {
446-
const result = new Array(keys.length);
455+
values (keys) {
456+
if (keys === undefined) {
457+
keys = this.keys();
458+
}
459+
460+
const result = Array.from({ length: keys.length });
447461
for (let i = 0; i < keys.length; i++) {
448-
result[i] = this.get(keys[i]);
462+
const item = this.items[keys[i]];
463+
result[i] = item !== undefined ? item.value : undefined;
449464
}
450465

451466
return result;

dist/tiny-lru.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/tiny-lru.min.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)