Skip to content
37 changes: 34 additions & 3 deletions src/components/Assesment/Assesment.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import panda from "../../assets/images/panda.svg";
import cryPanda from "../../assets/images/cryPanda.svg";
import { uniqueId } from "../../services/utilService";
import { end } from "../../services/telementryService";
import { useMediaCache } from "../Hooks/useMediaCache";

export const LanguageModal = ({ lang, setLang, setOpenLangModal }) => {
const [selectedLang, setSelectedLang] = useState(lang);
Expand Down Expand Up @@ -552,6 +553,36 @@ const Assesment = ({ discoverStart }) => {
username = userDetails.student_name;
setLocalData("profileName", username);
}

const { cacheMedia } = useMediaCache();
const [mediaUrls, setMediaUrls] = useState({});
const mediaFiles = [
{ key: "desktopLevel1", url: desktopLevel1 },
{ key: "desktopLevel2", url: desktopLevel2 },
{ key: "desktopLevel3", url: desktopLevel3 },
{ key: "desktopLevel4", url: desktopLevel4 },
{ key: "desktopLevel5", url: desktopLevel5 },
{ key: "desktopLevel6", url: desktopLevel6 },
{ key: "desktopLevel7", url: desktopLevel7 },
{ key: "desktopLevel8", url: desktopLevel8 },
{ key: "desktopLevel9", url: desktopLevel9 },
{ key: "assessmentBackground", url: assessmentBackground },
{ key: "HelpLogo", url: HelpLogo },
];

useEffect(() => {
const cacheAllMedia = async () => {
const urls = {};
for (const media of mediaFiles) {
const cachedUrl = await cacheMedia(media.key, media.url);
urls[media.key] = cachedUrl;
}
setMediaUrls(urls);
};

cacheAllMedia();
}, []);

// const [searchParams, setSearchParams] = useSearchParams();
// const [profileName, setProfileName] = useState(username);
const [openMessageDialog, setOpenMessageDialog] = useState("");
Expand Down Expand Up @@ -714,7 +745,7 @@ const Assesment = ({ discoverStart }) => {
const sectionStyle = {
width: "100vw",
height: "100vh",
backgroundImage: `url(${images?.[`desktopLevel${level || 1}`]})`,
backgroundImage: `url(${mediaUrls?.desktopLevel1})`,
backgroundRepeat: "round",
backgroundSize: "auto",
position: "relative",
Expand Down Expand Up @@ -803,7 +834,7 @@ const Assesment = ({ discoverStart }) => {
<MainLayout
showNext={false}
showTimer={false}
cardBackground={assessmentBackground}
cardBackground={mediaUrls?.assessmentBackground}
backgroundImage={practicebg}
{...{
setOpenLangModal,
Expand Down Expand Up @@ -865,7 +896,7 @@ const Assesment = ({ discoverStart }) => {
textAlign: "center",
}}
>
<img src={HelpLogo} alt="help_video_link" />
<img src={mediaUrls?.HelpLogo} alt="help_video_link" />
</Box>
)}
<Box
Expand Down
89 changes: 89 additions & 0 deletions src/components/Hooks/useMediaCache.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useEffect } from "react";

export function useMediaCache(dbName = "MediaCacheDB", storeName = "media") {
useEffect(() => {
initializeDB(dbName, storeName);
}, [dbName, storeName]);

const initializeDB = (dbName, storeName) => {
const openDB = indexedDB.open(dbName, 1);

openDB.onupgradeneeded = () => {
const db = openDB.result;
if (!db.objectStoreNames.contains(storeName)) {
db.createObjectStore(storeName, { keyPath: "key" });
}
};

openDB.onerror = (e) => {
console.error("IndexedDB initialization error:", e.target.error);
};
};

const saveMedia = async (key, blob) => {
const db = await openDatabase(dbName);
return await putMedia(db, storeName, { key, blob });
};

const getMedia = async (key) => {
const db = await openDatabase(dbName);
return await fetchMedia(db, storeName, key);
};

const cacheMedia = async (key, url) => {
try {
const cachedBlob = await getMedia(key);
if (cachedBlob) {
return URL.createObjectURL(cachedBlob);
}
return await fetchAndCacheMedia(key, url);
} catch (error) {
console.error(`Error caching media for key "${key}":`, error);
throw error;
}
};
Comment on lines +33 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Prevent memory leaks and add cache management

The caching implementation has potential memory leak issues and lacks important safeguards:

  1. URLs created with URL.createObjectURL are not revoked
  2. No cache size limits or cleanup strategy
  3. Missing media type validation
  4. No timeout handling for fetch operations

Apply these critical fixes:

 const cacheMedia = async (key, url) => {
   try {
     const cachedBlob = await getMedia(key);
     if (cachedBlob) {
-      return URL.createObjectURL(cachedBlob);
+      const objectUrl = URL.createObjectURL(cachedBlob);
+      // Store URL for cleanup
+      mediaUrls.set(key, objectUrl);
+      return objectUrl;
     }
-    return await fetchAndCacheMedia(key, url);
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 5000);
+    
+    try {
+      return await fetchAndCacheMedia(key, url, controller.signal);
+    } finally {
+      clearTimeout(timeoutId);
+    }
   } catch (error) {
+    if (error.name === 'AbortError') {
+      throw new Error(`Fetch timeout for media "${key}"`);
+    }
     console.error(`Error caching media for key "${key}":`, error);
     throw error;
   }
 };

Add these utility functions for proper resource management:

const mediaUrls = new Map();

const cleanupObjectUrl = (key) => {
  const url = mediaUrls.get(key);
  if (url) {
    URL.revokeObjectURL(url);
    mediaUrls.delete(key);
  }
};

// Add this to the returned object
return { 
  cacheMedia,
  cleanup: () => {
    mediaUrls.forEach((url) => URL.revokeObjectURL(url));
    mediaUrls.clear();
  }
};


const openDatabase = (dbName) => {
return new Promise((resolve, reject) => {
const openDB = indexedDB.open(dbName, 1);

openDB.onsuccess = () => resolve(openDB.result);
openDB.onerror = () => reject(openDB.error);
});
};

const putMedia = (db, storeName, media) => {
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, "readwrite");
const store = tx.objectStore(storeName);
const putRequest = store.put(media);

putRequest.onsuccess = () => resolve(true);
putRequest.onerror = () => reject(putRequest.error);

tx.oncomplete = () => db.close();
});
};

const fetchMedia = (db, storeName, key) => {
return new Promise((resolve, reject) => {
const tx = db.transaction(storeName, "readonly");
const store = tx.objectStore(storeName);
const getRequest = store.get(key);

getRequest.onsuccess = () => resolve(getRequest.result?.blob || null);
getRequest.onerror = () => reject(getRequest.error);

tx.oncomplete = () => db.close();
});
};

const fetchAndCacheMedia = async (key, url) => {
const response = await fetch(url);
const blob = await response.blob();
await saveMedia(key, blob);
return URL.createObjectURL(blob);
};
Comment on lines +81 to +86
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and add validation in fetchAndCacheMedia

The function needs better error handling and validation:

  1. No validation of response status
  2. No retry mechanism for failed fetches
  3. No size limits on cached blobs

Apply these improvements:

 const fetchAndCacheMedia = async (key, url) => {
-  const response = await fetch(url);
-  const blob = await response.blob();
-  await saveMedia(key, blob);
-  return URL.createObjectURL(blob);
+  const MAX_RETRIES = 3;
+  const MAX_BLOB_SIZE = 5 * 1024 * 1024; // 5MB
+  
+  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+    try {
+      const response = await fetch(url);
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`);
+      }
+      
+      const blob = await response.blob();
+      if (blob.size > MAX_BLOB_SIZE) {
+        throw new Error('Media file too large');
+      }
+      
+      await saveMedia(key, blob);
+      const objectUrl = URL.createObjectURL(blob);
+      mediaUrls.set(key, objectUrl);
+      return objectUrl;
+    } catch (error) {
+      if (attempt === MAX_RETRIES) throw error;
+      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
+    }
+  }
 };

Committable suggestion skipped: line range outside the PR's diff.


return { cacheMedia };
}
70 changes: 54 additions & 16 deletions src/components/Layouts.jsx/MainLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import textureImage from "../../assets/images/textureImage.png";
import timer from "../../assets/images/timer.svg";
import playButton from "../../assets/listen.png";
import pauseButton from "../../assets/pause.png";
import { useMediaCache } from "../Hooks/useMediaCache";
import {
GreenTick,
HeartBlack,
Expand Down Expand Up @@ -46,55 +47,88 @@ import { useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";

const MainLayout = (props) => {
const [mediaUrls, setMediaUrls] = useState({});
const { cacheMedia } = useMediaCache();

const mediaFiles = [
{ key: "practicebgstone", url: practicebgstone },
{ key: "practicebgstone2", url: practicebgstone2 },
{ key: "practicebgstone3", url: practicebgstone3 },
{ key: "practicebg", url: practicebg },
{ key: "practicebg2", url: practicebg2 },
{ key: "practicebg3", url: practicebg3 },
{ key: "gameWon", url: gameWon },
{ key: "gameLost", url: gameLost },
{ key: "textureImage", url: textureImage },
{ key: "timer", url: timer },
{ key: "playButton", url: playButton },
{ key: "playButton", url: playButton },
Comment on lines +64 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove duplicate entry for playButton.

The mediaFiles array contains a duplicate entry for playButton, which could lead to unnecessary caching operations.

   { key: "playButton", url: playButton },
-  { key: "playButton", url: playButton },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{ key: "playButton", url: playButton },
{ key: "playButton", url: playButton },
{ key: "playButton", url: playButton },

{ key: "clouds", url: clouds },
{ key: "catLoading", url: catLoading },
];

useEffect(() => {
const cacheAllMedia = async () => {
const urls = {};
for (const media of mediaFiles) {
const cachedUrl = await cacheMedia(media.key, media.url);
urls[media.key] = cachedUrl;
}
setMediaUrls(urls);
};

cacheAllMedia();
}, []);

const levelsImages = {
1: {
milestone: <LevelOne />,
backgroundAddOn: practicebgstone,
backgroundAddOn: mediaUrls.practicebgstone,
background: practicebg,
},
2: {
milestone: <LevelTwo />,
backgroundAddOn: practicebgstone2,
backgroundAddOn: mediaUrls.practicebgstone2,
background: practicebg2,
},
3: {
milestone: <LevelThree />,
backgroundAddOn: practicebgstone3,
backgroundAddOn: mediaUrls.practicebgstone3,
background: practicebg3,
},
4: {
milestone: <LevelFour />,
backgroundAddOn: practicebgstone,
backgroundAddOn: mediaUrls.practicebgstone,
background: practicebg3,
backgroundColor: `${levelConfig[4].color}60`,
},
5: {
milestone: <LevelFive />,
backgroundAddOn: practicebgstone3,
backgroundAddOn: mediaUrls.practicebgstone3,
background: practicebg3,
backgroundColor: `${levelConfig[5].color}60`,
},
6: {
milestone: <LevelSix />,
backgroundAddOn: practicebgstone3,
backgroundAddOn: mediaUrls.practicebgstone3,
background: practicebg3,
backgroundColor: `${levelConfig[6].color}60`,
},
7: {
milestone: <LevelSeven />,
backgroundAddOn: practicebgstone3,
backgroundAddOn: mediaUrls.practicebgstone3,
background: practicebg3,
backgroundColor: `${levelConfig[7].color}60`,
},
8: {
milestone: <LevelEight />,
backgroundAddOn: practicebgstone3,
backgroundAddOn: mediaUrls.practicebgstone3,
background: practicebg3,
backgroundColor: `${levelConfig[8].color}60`,
},
9: {
milestone: <LevelNine />,
backgroundAddOn: practicebgstone3,
backgroundAddOn: mediaUrls.practicebgstone3,
background: practicebg3,
backgroundColor: `${levelConfig[9].color}60`,
},
Expand Down Expand Up @@ -327,7 +361,7 @@ const MainLayout = (props) => {
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
backgroundImage: `url(${cardBackground || textureImage})`,
backgroundImage: `url(${cardBackground || mediaUrls.textureImage})`,
backgroundSize: "contain",
backgroundRepeat: "round",
boxShadow: "0px 4px 20px -1px rgba(0, 0, 0, 0.00)",
Expand All @@ -337,7 +371,7 @@ const MainLayout = (props) => {
>
<Box>
<img
src={catLoading}
src={mediaUrls?.catLoading}
alt="catLoading"
// sx={{ height: "58px", width: "58px" }}
/>
Expand All @@ -356,7 +390,9 @@ const MainLayout = (props) => {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
backgroundImage: `url(${cardBackground || textureImage})`,
backgroundImage: `url(${
cardBackground || mediaUrls?.textureImage
})`,
backgroundRepeat: "no-repeat",
backgroundSize: "cover",
boxShadow: "0px 4px 20px -1px rgba(0, 0, 0, 0.00)",
Expand All @@ -374,7 +410,7 @@ const MainLayout = (props) => {
{showTimer && (
<Box sx={{ position: "absolute" }}>
<img
src={timer}
src={mediaUrls?.timer}
alt="timer"
style={{ height: "58px", width: "58px" }}
/>
Expand Down Expand Up @@ -706,7 +742,9 @@ const MainLayout = (props) => {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
backgroundImage: `url(${cardBackground || textureImage})`,
backgroundImage: `url(${
cardBackground || mediaUrls.textureImage
})`,
backgroundSize: "contain",
backgroundRepeat: "round",
boxShadow: "0px 4px 20px -1px rgba(0, 0, 0, 0.00)",
Expand Down Expand Up @@ -766,7 +804,7 @@ const MainLayout = (props) => {
>
{!gameOverData?.userWon && (
<img
src={clouds}
src={mediaUrls?.clouds}
alt="clouds"
style={{ zIndex: -999 }}
/>
Expand All @@ -782,7 +820,7 @@ const MainLayout = (props) => {
>
{gameOverData?.userWon ? (
<img
src={gameWon}
src={mediaUrls?.gameWon}
alt="gameWon"
style={{ zIndex: 9999, height: 340 }}
/>
Expand Down
1 change: 0 additions & 1 deletion src/views/Practice/Practice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
import axios from "axios";
import WordsOrImage from "../../components/Mechanism/WordsOrImage";
import { uniqueId } from "../../services/utilService";
import useSound from "use-sound";
import LevelCompleteAudio from "../../assets/audio/levelComplete.wav";
import { splitGraphemes } from "split-graphemes";
import { Typography } from "@mui/material";
Expand Down