11
22import axios from 'axios'
33
4+ interface CacheEntry {
5+ objectUrl : string
6+ size : number
7+ lastUsed : number
8+ }
9+
410class 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