Skip to content

Commit 1eb030f

Browse files
committed
fix(core): ClientCacheMemory.findCells never stops
1 parent 1350670 commit 1eb030f

File tree

5 files changed

+372
-46
lines changed

5 files changed

+372
-46
lines changed

.changeset/cute-views-strive.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ckb-ccc/core": patch
3+
---
4+
5+
fix(core): `ClientCacheMemory.findCells` never stops
6+
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { describe, expect, it } from "vitest";
2+
import { MapLru } from "./memory.advanced.js";
3+
4+
describe("MapLru", () => {
5+
it("should throw an error for invalid capacity", () => {
6+
expect(() => new MapLru(0)).toThrow("Capacity must be a positive integer");
7+
expect(() => new MapLru(-1)).toThrow("Capacity must be a positive integer");
8+
expect(() => new MapLru(1.5)).toThrow(
9+
"Capacity must be a positive integer",
10+
);
11+
});
12+
13+
it("should set and get values correctly", () => {
14+
const cache = new MapLru<string, number>(2);
15+
cache.set("a", 1);
16+
cache.set("b", 2);
17+
18+
expect(cache.get("a")).toBe(1);
19+
expect(cache.get("b")).toBe(2);
20+
expect(cache.size).toBe(2);
21+
});
22+
23+
it("should evict the least recently used item when capacity is exceeded", () => {
24+
const cache = new MapLru<string, number>(2);
25+
cache.set("a", 1);
26+
cache.set("b", 2);
27+
cache.set("c", 3); // This should evict "a"
28+
29+
expect(cache.has("a")).toBe(false);
30+
expect(cache.get("b")).toBe(2);
31+
expect(cache.get("c")).toBe(3);
32+
expect(cache.size).toBe(2);
33+
});
34+
35+
it("should update the recently used status on get, affecting eviction order", () => {
36+
const cache = new MapLru<string, number>(2);
37+
cache.set("a", 1);
38+
cache.set("b", 2);
39+
cache.get("a"); // "a" is now the most recently used
40+
cache.set("c", 3); // This should evict "b"
41+
42+
expect(cache.has("b")).toBe(false);
43+
expect(cache.get("a")).toBe(1);
44+
expect(cache.get("c")).toBe(3);
45+
expect(cache.size).toBe(2);
46+
});
47+
48+
it("should handle deletion correctly", () => {
49+
const cache = new MapLru<string, number>(2);
50+
cache.set("a", 1);
51+
cache.set("b", 2);
52+
cache.delete("a");
53+
54+
expect(cache.has("a")).toBe(false);
55+
expect(cache.get("b")).toBe(2);
56+
expect(cache.size).toBe(1);
57+
58+
// @ts-expect-error - accessing private property for testing
59+
expect(cache.lru.has("a")).toBe(false);
60+
});
61+
62+
describe("iteration behavior", () => {
63+
it("should handle LRU updates when an item is accessed during iteration", () => {
64+
const cache = new MapLru<string, number>(3);
65+
cache.set("a", 1).set("b", 2).set("c", 3);
66+
67+
// Initial LRU order: a, b, c (c is MRU)
68+
// @ts-expect-error - accessing private property for testing
69+
expect(Array.from(cache.lru.keys())).toEqual(["a", "b", "c"]);
70+
71+
for (const [key] of cache.entries()) {
72+
if (key === "b") {
73+
cache.get("a"); // Access 'a', making it the new MRU
74+
}
75+
}
76+
77+
// Final LRU order should be: b, c, a
78+
// @ts-expect-error - accessing private property for testing
79+
expect(Array.from(cache.lru.keys())).toEqual(["b", "c", "a"]);
80+
81+
// Adding a new item should evict the new LRU item, which is 'b'
82+
cache.set("d", 4);
83+
expect(cache.has("b")).toBe(false);
84+
expect(cache.has("c")).toBe(true);
85+
expect(cache.has("a")).toBe(true);
86+
expect(cache.has("d")).toBe(true);
87+
});
88+
89+
it("should handle modifications and evictions during iteration", () => {
90+
const cache = new MapLru<string, number>(3);
91+
cache.set("a", 1).set("b", 2).set("c", 3);
92+
93+
// Initial state: keys are [a, b, c], LRU order is [a, b, c]
94+
const visited: string[] = [];
95+
// The standard Map iterator will visit newly added items.
96+
// When we add "d", "a" gets evicted. The iterator, having already passed "a",
97+
// will continue to "c" and then visit the new item "d".
98+
for (const [key] of cache.entries()) {
99+
visited.push(key);
100+
if (key === "b") {
101+
cache.set("d", 4); // This will evict 'a'
102+
}
103+
}
104+
105+
expect(visited).toEqual(["a", "b", "c", "d"]);
106+
107+
// Final state of the cache
108+
expect(cache.has("a")).toBe(false);
109+
expect(cache.size).toBe(3);
110+
expect(Array.from(cache.keys())).toEqual(["b", "c", "d"]);
111+
});
112+
});
113+
});

packages/core/src/client/cache/memory.advanced.ts

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -145,73 +145,80 @@ export function filterCell(
145145
}
146146

147147
/**
148-
* A Least Recently Used (LRU) cache implemented using a Map.
149-
*
150-
* This class extends the built-in Map to provide an LRU cache with a fixed capacity.
151-
* When the cache is full, the least recently used entry is automatically evicted.
152-
*
153-
* @template K The type of the keys in the cache.
154-
* @template V The type of the values in the cache.
148+
* A Map-like class that implements a "Least Recently Used" (LRU) cache policy.
149+
* When the cache reaches its capacity, the least recently used item is removed.
150+
* @public
155151
*/
156152
export class MapLru<K, V> extends Map<K, V> {
153+
private readonly lru: Set<K> = new Set<K>();
154+
157155
/**
158-
* Constructs a new MapLru instance.
159-
*
160-
* @param capacity The maximum number of entries the cache can hold. Must be a positive integer.
161-
* @throws {Error} If the capacity is not a positive integer.
156+
* @param capacity - The maximum number of items to store in the cache. Must be a positive integer.
162157
*/
163158
constructor(private readonly capacity: number) {
164159
super();
160+
165161
if (!Number.isInteger(capacity) || capacity < 1) {
166162
throw new Error("Capacity must be a positive integer");
167163
}
168164
}
169165

170166
/**
171-
* Retrieves a value from the cache.
172-
*
173-
* If the key is present in the cache, the value is moved to the most-recently-used position.
174-
*
175-
* @param key The key of the value to retrieve.
176-
* @returns The value associated with the key, or undefined if the key is not present.
167+
* Retrieves the value for a given key and marks it as recently used.
168+
* @param key - The key of the element to retrieve.
169+
* @returns The value associated with the key, or `undefined` if the key is not in the cache.
177170
*/
178-
override get(key: K): V | undefined {
179-
// Check if the key exists. If not, return undefined.
171+
override get(key: K) {
180172
if (!super.has(key)) {
181-
return undefined;
173+
return;
182174
}
183175

184-
const value = super.get(key) as V;
176+
this.lru.delete(key);
177+
this.lru.add(key);
185178

186-
// Move to most-recently-used position
187-
super.delete(key);
188-
super.set(key, value);
189-
190-
return value;
179+
return super.get(key);
191180
}
192181

193182
/**
194-
* Inserts a new value into the cache, or updates an existing value.
195-
*
196-
* If the key is already present in the cache, it is first deleted so that the re-insertion
197-
* moves it to the most-recently-used position.
198-
* If the cache is over capacity after the insertion, the least recently used entry is evicted.
199-
*
200-
* @param key The key of the value to insert or update.
201-
* @param value The value to associate with the key.
202-
* @returns This MapLru instance.
183+
* Adds or updates a key-value pair in the cache and marks the key as recently used.
184+
* If setting a new key causes the cache to exceed its capacity, the least recently used item is evicted.
185+
* @param key - The key of the element to add or update.
186+
* @param value - The value of the element to add or update.
187+
* @returns The `MapLru` instance.
203188
*/
204-
override set(key: K, value: V): this {
205-
// Delete and re-insert to move key to the end (most-recently-used)
206-
super.delete(key);
189+
override set(key: K, value: V) {
207190
super.set(key, value);
208191

209-
// Evict oldest if over capacity
210-
if (super.size > this.capacity) {
211-
const oldestKey = super.keys().next().value!;
212-
super.delete(oldestKey);
213-
}
192+
this.lru.delete(key);
193+
this.lru.add(key);
214194

195+
// Evict the oldest entry if capacity is exceeded.
196+
if (this.lru.size > this.capacity) {
197+
const oldestKey = this.lru.keys().next().value!;
198+
this.delete(oldestKey);
199+
}
215200
return this;
216201
}
202+
203+
/**
204+
* Removes the specified element from the cache.
205+
* @param key - The key of the element to remove.
206+
* @returns `true` if an element in the `MapLru` object existed and has been removed, or `false` if the element does not exist.
207+
*/
208+
override delete(key: K): boolean {
209+
if (!super.delete(key)) {
210+
return false;
211+
}
212+
213+
this.lru.delete(key);
214+
return true;
215+
}
216+
217+
/**
218+
* Removes all key-value pairs from the cache.
219+
*/
220+
override clear() {
221+
super.clear();
222+
this.lru.clear();
223+
}
217224
}

0 commit comments

Comments
 (0)