mirror of
https://github.com/mue/mue.git
synced 2026-07-05 23:51:12 +02:00
feat: implement safe JSON parsing and caching utilities; refactor background and quote loaders
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"@/*": ["./*"],
|
||||
"i18n/*": ["./i18n/*"],
|
||||
"components/*": ["./components/*"],
|
||||
"hooks/*": ["./hooks/*"],
|
||||
"assets/*": ["./assets/*"],
|
||||
"config/*": ["./config/*"],
|
||||
"features/*": ["./features/*"],
|
||||
|
||||
@@ -6,19 +6,7 @@ import videoCheck from './videoCheck';
|
||||
import { getAllBackgrounds, getAllBackgroundsWithMetadata } from 'utils/customBackgroundDB';
|
||||
import { BackgroundQueueManager } from 'utils/backgroundQueue';
|
||||
import { getProxiedImageUrl } from 'utils/marketplace';
|
||||
|
||||
const parseJSON = (key, fallback = null) => {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item === null || item === 'null') {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(item);
|
||||
return parsed !== null ? parsed : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
import { safeParseJSON } from 'utils/jsonStorage';
|
||||
|
||||
/**
|
||||
* Fetches image data from the configured API
|
||||
@@ -26,8 +14,11 @@ const parseJSON = (key, fallback = null) => {
|
||||
export async function fetchAPIImageData(excludedPun = null) {
|
||||
const api = localStorage.getItem('backgroundAPI') || 'mue';
|
||||
const quality = localStorage.getItem('apiQuality') || 'high';
|
||||
const categories = parseJSON('apiCategories', localStorage.getItem('apiCategories'));
|
||||
const excludes = [...parseJSON('backgroundExclude', []), ...(excludedPun ? [excludedPun] : [])];
|
||||
const categories = safeParseJSON('apiCategories', localStorage.getItem('apiCategories'));
|
||||
const excludes = [
|
||||
...safeParseJSON('backgroundExclude', []),
|
||||
...(excludedPun ? [excludedPun] : []),
|
||||
];
|
||||
|
||||
const baseURL = `${variables.constants.API_URL}/images`;
|
||||
const collection = localStorage.getItem('unsplashCollections');
|
||||
@@ -80,7 +71,7 @@ export async function getBackgroundData() {
|
||||
localStorage.getItem('showWelcome') === 'true';
|
||||
|
||||
// Handle favourited background
|
||||
const fav = parseJSON('favourite');
|
||||
const fav = safeParseJSON('favourite');
|
||||
if (fav) {
|
||||
if (fav.type === 'random_colour' || fav.type === 'random_gradient') {
|
||||
return { type: 'colour', style: `background:${fav.url}` };
|
||||
@@ -192,7 +183,7 @@ async function getCustomBackground(isOffline) {
|
||||
|
||||
// Fallback to localStorage URLs if IndexedDB is empty
|
||||
if (!backgrounds || backgrounds.length === 0) {
|
||||
const urls = parseJSON('customBackground', []);
|
||||
const urls = safeParseJSON('customBackground', []);
|
||||
if (urls && urls.length > 0) {
|
||||
// Convert old URL format to metadata format
|
||||
backgrounds = urls.map((url) => ({ url, photoInfo: { hidden: true } }));
|
||||
@@ -259,9 +250,11 @@ async function getCustomBackground(isOffline) {
|
||||
|
||||
// Prefetch more backgrounds in the background (skip videos)
|
||||
if (queueManager.needsPrefetch() && !data.video && selected.id) {
|
||||
prefetchCustomBackgrounds(queueManager, backgrounds, selected.id, cachedQueue).catch((error) => {
|
||||
console.error('Failed to prefetch custom backgrounds:', error);
|
||||
});
|
||||
prefetchCustomBackgrounds(queueManager, backgrounds, selected.id, cachedQueue).catch(
|
||||
(error) => {
|
||||
console.error('Failed to prefetch custom backgrounds:', error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -306,7 +299,7 @@ async function prefetchCustomBackgrounds(queueManager, allBackgrounds, currentId
|
||||
function getPhotoPackBackground(isOffline) {
|
||||
if (isOffline) return getOfflineImage('photo_pack');
|
||||
|
||||
const photos = parseJSON('installed', []).flatMap((item) =>
|
||||
const photos = safeParseJSON('installed', []).flatMap((item) =>
|
||||
item.type === 'photos' && item.photos ? item.photos : [],
|
||||
);
|
||||
|
||||
@@ -364,10 +357,7 @@ async function prefetchPhotoPackImages(queueManager, allPhotos, currentPhoto, cu
|
||||
const count = queueManager.getSpaceNeeded();
|
||||
|
||||
// Get already used URLs
|
||||
const usedUrls = [
|
||||
currentPhoto.url,
|
||||
...currentQueue.map((p) => p.url),
|
||||
];
|
||||
const usedUrls = [currentPhoto.url, ...currentQueue.map((p) => p.url)];
|
||||
|
||||
// Filter available photos
|
||||
const available = allPhotos.filter((p) => !usedUrls.includes(p.url.default));
|
||||
|
||||
@@ -2,11 +2,21 @@ import { useCallback } from 'react';
|
||||
import variables from 'config/variables';
|
||||
import offline_quotes from '../offline_quotes.json';
|
||||
import { shouldUpdateByFrequency, resetStartTime } from 'utils/frequencyManager';
|
||||
import { safeParseJSON } from 'utils/jsonStorage';
|
||||
import { useCachedFetch } from 'hooks/useCachedFetch';
|
||||
import { QueueManager } from 'utils/backgroundQueue';
|
||||
|
||||
/**
|
||||
* Custom hook for loading quote data from various sources
|
||||
*/
|
||||
export function useQuoteLoader(updateQuote) {
|
||||
// Initialize cache hook for author images
|
||||
const { fetchWithCache: fetchAuthorImage } = useCachedFetch({
|
||||
cacheKey: 'authorImageCache',
|
||||
timestampKey: 'authorImageCacheTimestamp',
|
||||
expiryDays: 7,
|
||||
});
|
||||
|
||||
const getAuthorLink = useCallback((author) => {
|
||||
return localStorage.getItem('authorLink') === 'false' || author === 'Unknown'
|
||||
? null
|
||||
@@ -20,30 +30,6 @@ export function useQuoteLoader(updateQuote) {
|
||||
return tmpdoc.body.textContent || '';
|
||||
}, []);
|
||||
|
||||
// Parse JSON safely with fallback
|
||||
const parseJSON = useCallback((key, fallback = null) => {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item === null || item === 'null') {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(item);
|
||||
return parsed !== null ? parsed : fallback;
|
||||
} catch {
|
||||
// Corrupt data - reinitialize
|
||||
localStorage.setItem(key, JSON.stringify(fallback));
|
||||
return fallback;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check if author image cache is still valid (7 day expiry)
|
||||
const isAuthorCacheValid = useCallback(() => {
|
||||
const timestamp = localStorage.getItem('authorImageCacheTimestamp');
|
||||
if (!timestamp) return false;
|
||||
const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
|
||||
return Date.now() - Number(timestamp) < SEVEN_DAYS;
|
||||
}, []);
|
||||
|
||||
const getAuthorImg = useCallback(
|
||||
async (author) => {
|
||||
if (localStorage.getItem('authorImg') === 'false' || author === 'Unknown') {
|
||||
@@ -53,8 +39,8 @@ export function useQuoteLoader(updateQuote) {
|
||||
try {
|
||||
const lang = variables.languagecode.split('_')[0];
|
||||
const pageData = await fetch(
|
||||
`https://${lang}.wikipedia.org/w/api.php?action=query&titles=${author}&origin=*&prop=pageimages&format=json&pithumbsize=100`
|
||||
).then(res => res.json());
|
||||
`https://${lang}.wikipedia.org/w/api.php?action=query&titles=${author}&origin=*&prop=pageimages&format=json&pithumbsize=100`,
|
||||
).then((res) => res.json());
|
||||
|
||||
const authorPage = Object.values(pageData.query.pages)[0];
|
||||
const authorimg = authorPage?.thumbnail?.source;
|
||||
@@ -64,13 +50,15 @@ export function useQuoteLoader(updateQuote) {
|
||||
}
|
||||
|
||||
const licenseData = await fetch(
|
||||
`https://${lang}.wikipedia.org/w/api.php?action=query&prop=imageinfo&iiprop=extmetadata&titles=File:${authorPage.pageimage}&origin=*&format=json`
|
||||
).then(res => res.json());
|
||||
`https://${lang}.wikipedia.org/w/api.php?action=query&prop=imageinfo&iiprop=extmetadata&titles=File:${authorPage.pageimage}&origin=*&format=json`,
|
||||
).then((res) => res.json());
|
||||
|
||||
const licensePage = Object.values(licenseData.query.pages)[0];
|
||||
const metadata = licensePage?.imageinfo?.[0]?.extmetadata;
|
||||
const license = metadata?.LicenseShortName;
|
||||
const photographer = stripHTML(metadata?.Attribution?.value || metadata?.Artist?.value || '')
|
||||
const photographer = stripHTML(
|
||||
metadata?.Attribution?.value || metadata?.Artist?.value || '',
|
||||
)
|
||||
.replace(/©\s/, '')
|
||||
.replace(/ \(talk\)/, '');
|
||||
|
||||
@@ -96,43 +84,20 @@ export function useQuoteLoader(updateQuote) {
|
||||
);
|
||||
|
||||
// Get cached author image or fetch and cache it
|
||||
const getCachedAuthorImg = useCallback(async (author) => {
|
||||
if (localStorage.getItem('authorImg') === 'false' || author === 'Unknown') {
|
||||
return { authorimg: null, authorimglicense: null };
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
const cache = parseJSON('authorImageCache', {});
|
||||
if (isAuthorCacheValid() && cache[author]) {
|
||||
return {
|
||||
authorimg: cache[author].authorimg,
|
||||
authorimglicense: cache[author].authorimglicense
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch from Wikipedia
|
||||
const result = await getAuthorImg(author);
|
||||
|
||||
// Cache the result (even if null, to avoid re-fetching)
|
||||
if (result.authorimg || result.authorimglicense) {
|
||||
cache[author] = {
|
||||
authorimg: result.authorimg,
|
||||
authorimglicense: result.authorimglicense,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
try {
|
||||
localStorage.setItem('authorImageCache', JSON.stringify(cache));
|
||||
localStorage.setItem('authorImageCacheTimestamp', Date.now().toString());
|
||||
} catch (e) {
|
||||
// Handle quota exceeded - clear old cache entries
|
||||
console.warn('Author image cache quota exceeded, clearing cache');
|
||||
localStorage.removeItem('authorImageCache');
|
||||
localStorage.setItem('authorImageCacheTimestamp', Date.now().toString());
|
||||
const getCachedAuthorImg = useCallback(
|
||||
async (author) => {
|
||||
if (localStorage.getItem('authorImg') === 'false' || author === 'Unknown') {
|
||||
return { authorimg: null, authorimglicense: null };
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [parseJSON, isAuthorCacheValid, getAuthorImg]);
|
||||
return await fetchAuthorImage(author, async () => {
|
||||
const result = await getAuthorImg(author);
|
||||
// Return only if we have actual data
|
||||
return result.authorimg || result.authorimglicense ? result : null;
|
||||
});
|
||||
},
|
||||
[fetchAuthorImage, getAuthorImg],
|
||||
);
|
||||
|
||||
// Select raw quote data without author images (non-blocking)
|
||||
const selectQuoteData = useCallback(() => {
|
||||
@@ -151,10 +116,12 @@ export function useQuoteLoader(updateQuote) {
|
||||
try {
|
||||
customQuote = JSON.parse(localStorage.getItem('customQuote'));
|
||||
} catch {
|
||||
customQuote = [{
|
||||
quote: localStorage.getItem('customQuote'),
|
||||
author: localStorage.getItem('customQuoteAuthor'),
|
||||
}];
|
||||
customQuote = [
|
||||
{
|
||||
quote: localStorage.getItem('customQuote'),
|
||||
author: localStorage.getItem('customQuoteAuthor'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (!customQuote || customQuote.length === 0) {
|
||||
@@ -184,13 +151,15 @@ export function useQuoteLoader(updateQuote) {
|
||||
|
||||
const installed = JSON.parse(localStorage.getItem('installed') || '[]');
|
||||
const quotePack = installed
|
||||
.filter(item => item.type === 'quotes')
|
||||
.flatMap(item => item.quotes.map(quote => ({
|
||||
...quote,
|
||||
fallbackauthorimg: item.icon_url,
|
||||
packName: item.display_name || item.name,
|
||||
noAuthorImg: item.noAuthorImg || quote.noAuthorImg,
|
||||
})));
|
||||
.filter((item) => item.type === 'quotes')
|
||||
.flatMap((item) =>
|
||||
item.quotes.map((quote) => ({
|
||||
...quote,
|
||||
fallbackauthorimg: item.icon_url,
|
||||
packName: item.display_name || item.name,
|
||||
noAuthorImg: item.noAuthorImg || quote.noAuthorImg,
|
||||
})),
|
||||
);
|
||||
|
||||
if (quotePack.length === 0) {
|
||||
const quote = offline_quotes[Math.floor(Math.random() * offline_quotes.length)];
|
||||
@@ -217,41 +186,44 @@ export function useQuoteLoader(updateQuote) {
|
||||
}, [getAuthorLink]);
|
||||
|
||||
// Fetch complete quote data including author image (for prefetching)
|
||||
const fetchCompleteQuote = useCallback(async (quoteData) => {
|
||||
if (!quoteData || quoteData.noQuote) return quoteData;
|
||||
const fetchCompleteQuote = useCallback(
|
||||
async (quoteData) => {
|
||||
if (!quoteData || quoteData.noQuote) return quoteData;
|
||||
|
||||
// If author image is needed, fetch it
|
||||
if (quoteData.needsAuthorImg) {
|
||||
const authorToLookup = quoteData.realAuthor || quoteData.author;
|
||||
try {
|
||||
const authorImgData = await getCachedAuthorImg(authorToLookup);
|
||||
// If author image is needed, fetch it
|
||||
if (quoteData.needsAuthorImg) {
|
||||
const authorToLookup = quoteData.realAuthor || quoteData.author;
|
||||
try {
|
||||
const authorImgData = await getCachedAuthorImg(authorToLookup);
|
||||
|
||||
// For quote packs, use fallback if Wikipedia fails
|
||||
if (!authorImgData.authorimg && quoteData.fallbackauthorimg) {
|
||||
return {
|
||||
...quoteData,
|
||||
authorimg: quoteData.fallbackauthorimg,
|
||||
authorimglicense: null,
|
||||
};
|
||||
}
|
||||
|
||||
// For quote packs, use fallback if Wikipedia fails
|
||||
if (!authorImgData.authorimg && quoteData.fallbackauthorimg) {
|
||||
return {
|
||||
...quoteData,
|
||||
authorimg: quoteData.fallbackauthorimg,
|
||||
...authorImgData,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch author image:', e);
|
||||
// Return quote data with fallback or no image
|
||||
return {
|
||||
...quoteData,
|
||||
authorimg: quoteData.fallbackauthorimg || null,
|
||||
authorimglicense: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...quoteData,
|
||||
...authorImgData,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch author image:', e);
|
||||
// Return quote data with fallback or no image
|
||||
return {
|
||||
...quoteData,
|
||||
authorimg: quoteData.fallbackauthorimg || null,
|
||||
authorimglicense: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return quoteData;
|
||||
}, [getCachedAuthorImg]);
|
||||
return quoteData;
|
||||
},
|
||||
[getCachedAuthorImg],
|
||||
);
|
||||
|
||||
const doOffline = useCallback(() => {
|
||||
const quote = offline_quotes[Math.floor(Math.random() * offline_quotes.length)];
|
||||
@@ -322,13 +294,13 @@ export function useQuoteLoader(updateQuote) {
|
||||
}
|
||||
|
||||
// MAIN FLOW: Use queue system
|
||||
const cachedQueue = parseJSON('quoteQueue', []);
|
||||
const queueManager = new QueueManager('quoteQueue', 3);
|
||||
const cachedQueue = queueManager.getQueue();
|
||||
let quoteData;
|
||||
|
||||
// Step 1: Try to get from queue
|
||||
if (cachedQueue.length > 0) {
|
||||
quoteData = cachedQueue.shift();
|
||||
localStorage.setItem('quoteQueue', JSON.stringify(cachedQueue));
|
||||
quoteData = queueManager.shift();
|
||||
} else {
|
||||
// Step 2: No queue, fetch new quote immediately
|
||||
const rawQuote = selectQuoteData();
|
||||
@@ -350,10 +322,12 @@ export function useQuoteLoader(updateQuote) {
|
||||
getCachedAuthorImg(authorToLookup).then((authorImgData) => {
|
||||
updateQuote({
|
||||
...rawQuote,
|
||||
...(authorImgData.authorimg ? authorImgData : {
|
||||
authorimg: rawQuote.fallbackauthorimg || null,
|
||||
authorimglicense: null,
|
||||
}),
|
||||
...(authorImgData.authorimg
|
||||
? authorImgData
|
||||
: {
|
||||
authorimg: rawQuote.fallbackauthorimg || null,
|
||||
authorimglicense: null,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -378,40 +352,27 @@ export function useQuoteLoader(updateQuote) {
|
||||
}
|
||||
|
||||
// Step 5: Prefetch next 3 quotes asynchronously (non-blocking)
|
||||
const targetQueueSize = 3;
|
||||
const currentQueueSize = cachedQueue.length;
|
||||
if (queueManager.needsPrefetch() && !offline) {
|
||||
const spaceNeeded = queueManager.getSpaceNeeded();
|
||||
|
||||
if (currentQueueSize < targetQueueSize && !offline) {
|
||||
Promise.all(
|
||||
Array.from({ length: targetQueueSize - currentQueueSize }, async () => {
|
||||
Array.from({ length: spaceNeeded }, async () => {
|
||||
const rawQuote = selectQuoteData();
|
||||
if (rawQuote.noQuote) return null;
|
||||
return await fetchCompleteQuote(rawQuote);
|
||||
})
|
||||
).then((newQuotes) => {
|
||||
const validQuotes = newQuotes.filter(Boolean);
|
||||
if (validQuotes.length > 0) {
|
||||
const updatedQueue = [...cachedQueue, ...validQuotes];
|
||||
try {
|
||||
localStorage.setItem('quoteQueue', JSON.stringify(updatedQueue));
|
||||
} catch (e) {
|
||||
console.warn('Quote queue quota exceeded:', e);
|
||||
// Keep only 2 quotes if quota exceeded
|
||||
localStorage.setItem('quoteQueue', JSON.stringify(updatedQueue.slice(0, 2)));
|
||||
}),
|
||||
)
|
||||
.then((newQuotes) => {
|
||||
const validQuotes = newQuotes.filter(Boolean);
|
||||
if (validQuotes.length > 0) {
|
||||
queueManager.push(validQuotes);
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('Failed to prefetch quotes:', error);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to prefetch quotes:', error);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
updateQuote,
|
||||
getAuthorLink,
|
||||
getCachedAuthorImg,
|
||||
selectQuoteData,
|
||||
fetchCompleteQuote,
|
||||
parseJSON,
|
||||
]);
|
||||
}, [updateQuote, getAuthorLink, getCachedAuthorImg, selectQuoteData, fetchCompleteQuote]);
|
||||
|
||||
return {
|
||||
getQuote,
|
||||
|
||||
77
src/hooks/useCachedFetch.js
Normal file
77
src/hooks/useCachedFetch.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useCallback } from 'react';
|
||||
import { safeParseJSON } from 'utils/jsonStorage';
|
||||
|
||||
/**
|
||||
* Custom hook for caching fetch results with expiry
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {string} options.cacheKey - The localStorage key for the cache
|
||||
* @param {string} options.timestampKey - The localStorage key for the cache timestamp
|
||||
* @param {number} options.expiryDays - Number of days before cache expires (default: 7)
|
||||
* @returns {Object} - Cache utilities
|
||||
*/
|
||||
export function useCachedFetch({ cacheKey, timestampKey, expiryDays = 7 }) {
|
||||
// Check if cache is still valid
|
||||
const isCacheValid = useCallback(() => {
|
||||
const timestamp = localStorage.getItem(timestampKey);
|
||||
if (!timestamp) return false;
|
||||
const expiryMs = expiryDays * 24 * 60 * 60 * 1000;
|
||||
return Date.now() - Number(timestamp) < expiryMs;
|
||||
}, [timestampKey, expiryDays]);
|
||||
|
||||
// Get cached data for a specific key
|
||||
const getCached = useCallback(
|
||||
(key) => {
|
||||
const cache = safeParseJSON(cacheKey, {});
|
||||
if (isCacheValid() && cache[key]) {
|
||||
return cache[key];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[cacheKey, isCacheValid],
|
||||
);
|
||||
|
||||
// Fetch with caching
|
||||
const fetchWithCache = useCallback(
|
||||
async (key, fetchFn) => {
|
||||
// Check cache first
|
||||
const cached = getCached(key);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// Fetch from source
|
||||
const result = await fetchFn(key);
|
||||
|
||||
// Cache the result (even if null/empty, to avoid re-fetching)
|
||||
if (result !== null && result !== undefined) {
|
||||
const cache = safeParseJSON(cacheKey, {});
|
||||
cache[key] = {
|
||||
...result,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(cacheKey, JSON.stringify(cache));
|
||||
localStorage.setItem(timestampKey, Date.now().toString());
|
||||
} catch (error) {
|
||||
console.warn('Failed to cache data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
[cacheKey, timestampKey, getCached],
|
||||
);
|
||||
|
||||
// Clear cache
|
||||
const clearCache = useCallback(() => {
|
||||
localStorage.removeItem(cacheKey);
|
||||
localStorage.removeItem(timestampKey);
|
||||
}, [cacheKey, timestampKey]);
|
||||
|
||||
return {
|
||||
isCacheValid,
|
||||
getCached,
|
||||
fetchWithCache,
|
||||
clearCache,
|
||||
};
|
||||
}
|
||||
@@ -1,39 +1,22 @@
|
||||
import { safeParseJSON } from './jsonStorage';
|
||||
|
||||
/**
|
||||
* Background Queue Manager
|
||||
* Manages prefetch queues for background images across all background types
|
||||
* Queue Manager
|
||||
* Manages prefetch queues for content across features (backgrounds, quotes, etc.)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper to safely parse JSON from localStorage
|
||||
* @param {string} key - localStorage key
|
||||
* @param {*} fallback - Fallback value if parsing fails
|
||||
* @returns {*} Parsed value or fallback
|
||||
*/
|
||||
function parseJSON(key, fallback = null) {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item === null || item === 'null') {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(item);
|
||||
return parsed !== null ? parsed : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages a prefetch queue for background images
|
||||
* Manages a prefetch queue for content items
|
||||
*
|
||||
* @class BackgroundQueueManager
|
||||
* @class QueueManager
|
||||
* @example
|
||||
* const queueManager = new BackgroundQueueManager('imageQueue', 3);
|
||||
* const queueManager = new QueueManager('imageQueue', 3);
|
||||
* const queue = queueManager.getQueue();
|
||||
* if (queue.length > 0) {
|
||||
* const nextImage = queueManager.shift();
|
||||
* }
|
||||
*/
|
||||
export class BackgroundQueueManager {
|
||||
export class QueueManager {
|
||||
/**
|
||||
* Creates a new queue manager
|
||||
* @param {string} storageKey - localStorage key for this queue
|
||||
@@ -50,7 +33,7 @@ export class BackgroundQueueManager {
|
||||
*/
|
||||
getQueue() {
|
||||
try {
|
||||
return parseJSON(this.storageKey, []);
|
||||
return safeParseJSON(this.storageKey, []);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to get queue from ${this.storageKey}:`, error);
|
||||
return [];
|
||||
@@ -149,4 +132,7 @@ export class BackgroundQueueManager {
|
||||
}
|
||||
}
|
||||
|
||||
export default BackgroundQueueManager;
|
||||
// Backwards compatibility export
|
||||
export const BackgroundQueueManager = QueueManager;
|
||||
|
||||
export default QueueManager;
|
||||
|
||||
23
src/utils/jsonStorage.js
Normal file
23
src/utils/jsonStorage.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Safely parse JSON from localStorage with fallback
|
||||
* @param {string} key - The localStorage key to retrieve
|
||||
* @param {*} fallback - The fallback value if parsing fails or key doesn't exist
|
||||
* @param {boolean} reinitialize - Whether to reinitialize corrupt data with fallback
|
||||
* @returns {*} The parsed value or fallback
|
||||
*/
|
||||
export function safeParseJSON(key, fallback = null, reinitialize = false) {
|
||||
const item = localStorage.getItem(key);
|
||||
if (item === null || item === 'null') {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(item);
|
||||
return parsed !== null ? parsed : fallback;
|
||||
} catch (error) {
|
||||
// Corrupt data - optionally reinitialize
|
||||
if (reinitialize && fallback !== null) {
|
||||
localStorage.setItem(key, JSON.stringify(fallback));
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -169,6 +169,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
i18n: path.resolve(__dirname, './src/i18n'),
|
||||
components: path.resolve(__dirname, './src/components'),
|
||||
contexts: path.resolve(__dirname, './src/contexts'),
|
||||
hooks: path.resolve(__dirname, './src/hooks'),
|
||||
assets: path.resolve(__dirname, './src/assets'),
|
||||
config: path.resolve(__dirname, './src/config'),
|
||||
features: path.resolve(__dirname, './src/features'),
|
||||
|
||||
Reference in New Issue
Block a user