Skip to content

Commit c1ad5da

Browse files
committed
defend against mutation while iterating
Fix: #212
1 parent 7eb6368 commit c1ad5da

File tree

2 files changed

+79
-7
lines changed

2 files changed

+79
-7
lines changed

Diff for: index.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,12 @@ class LRUCache {
244244
*indexes ({ allowStale = this.allowStale } = {}) {
245245
if (this.size) {
246246
for (let i = this.tail, j; true; ) {
247+
j = i === this.head
247248
if (allowStale || !this.isStale(i)) {
248249
yield i
249250
}
250-
if (i === this.head) {
251+
// either head now, or WAS head and head was deleted
252+
if (i === this.head || j && !this.isValidIndex(i)) {
251253
break
252254
} else {
253255
i = this.prev[i]
@@ -259,10 +261,12 @@ class LRUCache {
259261
*rindexes ({ allowStale = this.allowStale } = {}) {
260262
if (this.size) {
261263
for (let i = this.head, j; true; ) {
264+
j = i === this.tail
262265
if (allowStale || !this.isStale(i)) {
263266
yield i
264267
}
265-
if (i === this.tail) {
268+
// either the tail now, or WAS the tail, and deleted
269+
if (i === this.tail || j && !this.isValidIndex(i)) {
266270
break
267271
} else {
268272
i = this.next[i]
@@ -271,6 +275,10 @@ class LRUCache {
271275
}
272276
}
273277

278+
isValidIndex (index) {
279+
return this.keyMap.get(this.keyList[index]) === index
280+
}
281+
274282
*entries () {
275283
for (const i of this.indexes()) {
276284
yield [this.keyList[i], this.valList[i]]
@@ -335,16 +343,12 @@ class LRUCache {
335343

336344
purgeStale () {
337345
let deleted = false
338-
const toDelete = []
339346
for (const i of this.rindexes({ allowStale: true })) {
340347
if (this.isStale(i)) {
341-
toDelete.push(this.keyList[i])
348+
this.delete(this.keyList[i])
342349
deleted = true
343350
}
344351
}
345-
for (const k of toDelete) {
346-
this.delete(k)
347-
}
348352
return deleted
349353
}
350354

Diff for: test/delete-while-iterating.js

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const t = require('tap')
2+
const LRU = require('../')
3+
4+
t.beforeEach(t => {
5+
const c = new LRU({ max: 5 })
6+
c.set(0, 0)
7+
c.set(1, 1)
8+
c.set(2, 2)
9+
c.set(3, 3)
10+
c.set(4, 4)
11+
t.context = c
12+
})
13+
14+
t.test('delete evens', t => {
15+
const c = t.context
16+
t.same([...c.keys()], [4, 3, 2, 1, 0])
17+
18+
for (const k of c.keys()) {
19+
if (k % 2 === 0) {
20+
c.delete(k)
21+
}
22+
}
23+
24+
t.same([...c.keys()], [3, 1])
25+
t.end()
26+
})
27+
28+
t.test('delete odds', t => {
29+
const c = t.context
30+
t.same([...c.keys()], [4, 3, 2, 1, 0])
31+
32+
for (const k of c.keys()) {
33+
if (k % 2 === 1) {
34+
c.delete(k)
35+
}
36+
}
37+
38+
t.same([...c.keys()], [4, 2, 0])
39+
t.end()
40+
})
41+
42+
t.test('rdelete evens', t => {
43+
const c = t.context
44+
t.same([...c.keys()], [4, 3, 2, 1, 0])
45+
46+
for (const k of c.rkeys()) {
47+
if (k % 2 === 0) {
48+
c.delete(k)
49+
}
50+
}
51+
52+
t.same([...c.keys()], [3, 1])
53+
t.end()
54+
})
55+
56+
t.test('rdelete odds', t => {
57+
const c = t.context
58+
t.same([...c.keys()], [4, 3, 2, 1, 0])
59+
60+
for (const k of c.rkeys()) {
61+
if (k % 2 === 1) {
62+
c.delete(k)
63+
}
64+
}
65+
66+
t.same([...c.keys()], [4, 2, 0])
67+
t.end()
68+
})

0 commit comments

Comments
 (0)