99} from "../constants" ;
1010import type {
1111 LogoSource ,
12+ MeasurementResult ,
1213 NormalizedLogo ,
1314 UseLogoSoupOptions ,
1415 UseLogoSoupResult ,
@@ -24,6 +25,12 @@ import {
2425 normalizeSource ,
2526} from "../utils/normalize" ;
2627
28+ interface CachedEntry {
29+ img : HTMLImageElement ;
30+ measurement : MeasurementResult ;
31+ blobUrl ?: string ;
32+ }
33+
2734type State = {
2835 isLoading : boolean ;
2936 normalizedLogos : NormalizedLogo [ ] ;
@@ -59,6 +66,22 @@ const INITIAL_STATE: State = {
5966 error : null ,
6067} ;
6168
69+ function clearCache ( cache : Map < string , CachedEntry > ) {
70+ for ( const entry of cache . values ( ) ) {
71+ if ( entry . blobUrl ) URL . revokeObjectURL ( entry . blobUrl ) ;
72+ }
73+ cache . clear ( ) ;
74+ }
75+
76+ function pruneCache ( cache : Map < string , CachedEntry > , activeSrcs : Set < string > ) {
77+ for ( const [ src , entry ] of cache ) {
78+ if ( ! activeSrcs . has ( src ) ) {
79+ if ( entry . blobUrl ) URL . revokeObjectURL ( entry . blobUrl ) ;
80+ cache . delete ( src ) ;
81+ }
82+ }
83+ }
84+
6285export function useLogoSoup ( options : UseLogoSoupOptions ) : UseLogoSoupResult {
6386 const {
6487 logos,
@@ -78,48 +101,111 @@ export function useLogoSoup(options: UseLogoSoupOptions): UseLogoSoupResult {
78101 }
79102 const stableLogos = logosRef . current ;
80103
104+ const cacheRef = useRef ( new Map < string , CachedEntry > ( ) ) ;
105+ const cacheKeyRef = useRef ( {
106+ contrastThreshold : NaN ,
107+ densityAware : false ,
108+ } ) ;
109+
110+ useEffect ( ( ) => {
111+ return ( ) => clearCache ( cacheRef . current ) ;
112+ } , [ ] ) ;
113+
81114 useEffect ( ( ) => {
82115 if ( stableLogos . length === 0 ) {
83116 dispatch ( { type : "empty" } ) ;
84117 return ;
85118 }
86119
87- let cancelled = false ;
88- const blobUrls : string [ ] = [ ] ;
89- dispatch ( { type : "loading" } ) ;
120+ const cache = cacheRef . current ;
121+ const prevKey = cacheKeyRef . current ;
122+
123+ if (
124+ prevKey . contrastThreshold !== contrastThreshold ||
125+ prevKey . densityAware !== densityAware
126+ ) {
127+ clearCache ( cache ) ;
128+ cacheKeyRef . current = { contrastThreshold, densityAware } ;
129+ }
90130
91131 const sources : LogoSource [ ] = stableLogos . map ( normalizeSource ) ;
132+ const activeSrcs = new Set ( sources . map ( ( s ) => s . src ) ) ;
133+ pruneCache ( cache , activeSrcs ) ;
134+
135+ const allCached = sources . every ( ( s ) => cache . has ( s . src ) ) ;
136+ const needsCrop =
137+ cropToContent &&
138+ sources . some ( ( s ) => {
139+ const entry = cache . get ( s . src ) ;
140+ return entry && ! entry . blobUrl && entry . measurement . contentBox ;
141+ } ) ;
142+
143+ if ( allCached && ! needsCrop ) {
144+ const effectiveDensityFactor = densityAware ? densityFactor : 0 ;
145+ const results = sources . map ( ( source ) => {
146+ const entry = cache . get ( source . src ) ! ;
147+ const normalized = createNormalizedLogo (
148+ source ,
149+ entry . measurement ,
150+ baseSize ,
151+ scaleFactor ,
152+ effectiveDensityFactor ,
153+ ) ;
154+ if ( cropToContent && entry . blobUrl ) {
155+ normalized . croppedSrc = entry . blobUrl ;
156+ }
157+ return normalized ;
158+ } ) ;
159+ dispatch ( { type : "success" , normalizedLogos : results } ) ;
160+ return ;
161+ }
162+
163+ let cancelled = false ;
164+ if ( ! allCached ) {
165+ dispatch ( { type : "loading" } ) ;
166+ }
92167
93168 Promise . allSettled (
94169 sources . map ( async ( source ) => {
95- const img = await loadImage ( source . src ) ;
170+ let entry = cache . get ( source . src ) ;
96171
97- if ( cancelled ) throw new Error ( "cancelled" ) ;
172+ if ( ! entry ) {
173+ const img = await loadImage ( source . src ) ;
174+ if ( cancelled ) throw new Error ( "cancelled" ) ;
98175
99- const measurement = measureWithContentDetection (
100- img ,
101- contrastThreshold ,
102- densityAware ,
103- ) ;
176+ const measurement = measureWithContentDetection (
177+ img ,
178+ contrastThreshold ,
179+ densityAware ,
180+ ) ;
181+ entry = { img, measurement } ;
182+ cache . set ( source . src , entry ) ;
183+ }
104184
105185 const effectiveDensityFactor = densityAware ? densityFactor : 0 ;
106186
107187 const normalized = createNormalizedLogo (
108188 source ,
109- measurement ,
189+ entry . measurement ,
110190 baseSize ,
111191 scaleFactor ,
112192 effectiveDensityFactor ,
113193 ) ;
114194
115- if ( cropToContent && measurement . contentBox ) {
116- const url = await cropToBlobUrl ( img , measurement . contentBox ) ;
195+ if ( cropToContent && entry . measurement . contentBox && ! entry . blobUrl ) {
196+ const url = await cropToBlobUrl (
197+ entry . img ,
198+ entry . measurement . contentBox ,
199+ ) ;
117200 if ( cancelled ) {
118201 URL . revokeObjectURL ( url ) ;
119202 throw new Error ( "cancelled" ) ;
120203 }
121- blobUrls . push ( url ) ;
122- normalized . croppedSrc = url ;
204+ entry . blobUrl = url ;
205+ }
206+
207+ if ( cropToContent && entry . blobUrl ) {
208+ normalized . croppedSrc = entry . blobUrl ;
123209 }
124210
125211 return normalized ;
@@ -150,9 +236,6 @@ export function useLogoSoup(options: UseLogoSoupOptions): UseLogoSoupResult {
150236
151237 return ( ) => {
152238 cancelled = true ;
153- for ( const url of blobUrls ) {
154- URL . revokeObjectURL ( url ) ;
155- }
156239 } ;
157240 } , [
158241 stableLogos ,
0 commit comments