Skip to content

Commit 0cf9d37

Browse files
committed
feat: use LRU cache for assets
1 parent 327f5f2 commit 0cf9d37

2 files changed

Lines changed: 84 additions & 8 deletions

File tree

src/components/Character.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ watch(
151151
if (scriptData.value && imageSize.value.width > 0) {
152152
const img = new Image()
153153
img.crossOrigin = 'anonymous'
154-
img.src = assetUrl.value
154+
img.src = resourceManager.getResolvedUrl(assetUrl.value)
155155
img.onload = () => {
156156
composeCharacterImage(img, scriptData.value!, imageSize.value.width, imageSize.value.height)
157157
}

src/utils/resourceManager.ts

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,50 @@
11

22
import axios from 'axios'
33

4+
interface CacheEntry {
5+
objectUrl: string
6+
size: number
7+
lastUsed: number
8+
}
9+
410
class ResourceManager {
5-
// Map original URL -> Blob URL
6-
private blobUrls: Map<string, string> = new Map()
11+
// Map original URL -> CacheEntry
12+
private cache: Map<string, CacheEntry> = new Map()
713
private pendingRequests: Map<string, Promise<void>> = new Map()
814

15+
private currentSize: number = 0
16+
// Limit to 50MB (approx)
17+
private readonly MAX_SIZE: number = 50 * 1024 * 1024
18+
919
async preload(url: string): Promise<void> {
10-
if (this.blobUrls.has(url)) return
20+
if (this.cache.has(url)) {
21+
this.touch(url)
22+
return
23+
}
1124
if (this.pendingRequests.has(url)) return this.pendingRequests.get(url)
1225

1326
const promise = axios.get(url, { responseType: 'blob' })
1427
.then(response => {
15-
const blob = response.data
28+
const blob = response.data as Blob
29+
const size = blob.size
30+
31+
// If single file is larger than max size, don't cache it (or handle gracefully)
32+
if (size > this.MAX_SIZE) {
33+
console.warn(`Asset too large to cache: ${url} (${size} bytes)`)
34+
return
35+
}
36+
37+
// Evict if needed
38+
this.ensureSpace(size)
39+
1640
const objectUrl = URL.createObjectURL(blob)
17-
this.blobUrls.set(url, objectUrl)
41+
this.cache.set(url, {
42+
objectUrl,
43+
size,
44+
lastUsed: Date.now()
45+
})
46+
this.currentSize += size
47+
1848
this.pendingRequests.delete(url)
1949
})
2050
.catch(e => {
@@ -26,6 +56,39 @@ class ResourceManager {
2656
return promise
2757
}
2858

59+
private touch(url: string) {
60+
const entry = this.cache.get(url)
61+
if (entry) {
62+
// Update lastUsed and move to end of Map (LRU behavior)
63+
entry.lastUsed = Date.now()
64+
this.cache.delete(url)
65+
this.cache.set(url, entry)
66+
}
67+
}
68+
69+
private ensureSpace(requiredSize: number) {
70+
while (this.currentSize + requiredSize > this.MAX_SIZE && this.cache.size > 0) {
71+
// Map iterator yields in insertion order.
72+
// Since we re-insert on access (touch), the first item is the LRU.
73+
const iterator = this.cache.keys()
74+
const lruKey = iterator.next().value
75+
if (lruKey) {
76+
this.remove(lruKey)
77+
} else {
78+
break
79+
}
80+
}
81+
}
82+
83+
private remove(url: string) {
84+
const entry = this.cache.get(url)
85+
if (entry) {
86+
URL.revokeObjectURL(entry.objectUrl)
87+
this.currentSize -= entry.size
88+
this.cache.delete(url)
89+
}
90+
}
91+
2992
// Alias for compatibility, but now they do the same thing
3093
async preloadImage(url: string): Promise<void> {
3194
return this.preload(url)
@@ -36,11 +99,24 @@ class ResourceManager {
3699
}
37100

38101
getResolvedUrl(url: string): string {
39-
return this.blobUrls.get(url) || url
102+
if (this.cache.has(url)) {
103+
this.touch(url)
104+
return this.cache.get(url)!.objectUrl
105+
}
106+
return url
40107
}
41108

42109
isLoaded(url: string): boolean {
43-
return this.blobUrls.has(url)
110+
return this.cache.has(url)
111+
}
112+
113+
releaseAll(): void {
114+
this.cache.forEach((entry) => {
115+
URL.revokeObjectURL(entry.objectUrl)
116+
})
117+
this.cache.clear()
118+
this.currentSize = 0
119+
this.pendingRequests.clear()
44120
}
45121
}
46122

0 commit comments

Comments
 (0)