feat: Add detailed logging for user profile components and image metadata extraction; enhance color handling utilities

This commit is contained in:
2026-04-30 20:23:44 +10:00
parent cad4852878
commit 5d4d518af8
9 changed files with 299 additions and 48 deletions

View File

@@ -170,6 +170,8 @@ export function detectImageFormat(data: ArrayBuffer | Uint8Array): ImageFormat {
export function extractColorFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
const format = detectImageFormat(imageData);
console.log('[extractColorFromImage] Detected format:', format);
switch (format) {
case 'png':
return extractColorFromPNG(imageData);
@@ -211,14 +213,19 @@ export function extractBannerFromImage(imageData: ArrayBuffer | Uint8Array): str
export function extractMetadataFromImage(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
const format = detectImageFormat(imageData);
console.log('[extractMetadataFromImage] Detected format:', format);
switch (format) {
case 'png':
return extractMetadataFromPNG(imageData);
const pngMetadata = extractMetadataFromPNG(imageData);
console.log('[extractMetadataFromImage] PNG metadata:', pngMetadata);
return pngMetadata;
// For other formats, extract color only for now
case 'webp':
case 'jpeg':
case 'gif': {
const color = extractColorFromImage(imageData);
console.log('[extractMetadataFromImage] Non-PNG color:', color);
return color ? { color } : {};
}
default:
@@ -344,6 +351,10 @@ export function getExtension(format: ImageFormat): string {
const avatarMetadataCache = new LRUCache<string, { metadata: ImageMetadata; timestamp: number }>(100);
const METADATA_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
// In-flight fetch tracking to prevent duplicate parallel requests
const inflightColorFetches = new Map<string, Promise<string | undefined>>();
const inflightMetadataFetches = new Map<string, Promise<ImageMetadata>>();
/**
* Fetch and extract color from an image URL (supports all formats)
* @param url - HTTP URL to fetch the image from
@@ -354,29 +365,51 @@ export async function fetchAndExtractColor(url: string, accessToken?: string | n
// Check if full metadata is cached
const cached = avatarMetadataCache.get(url);
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
console.log('[fetchAndExtractColor] Using cached color for', url, ':', cached.metadata.color);
return cached.metadata.color;
}
try {
const response = await fetch(url, {
method: 'GET',
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
});
if (!response.ok) return undefined;
const data = await response.arrayBuffer();
const color = extractColorFromImage(data);
// Cache the color result as part of metadata
avatarMetadataCache.set(url, {
metadata: { color },
timestamp: Date.now()
});
return color;
} catch {
return undefined;
// Check if fetch already in progress
const inflightKey = `${url}:${accessToken || ''}`;
const existingFetch = inflightColorFetches.get(inflightKey);
if (existingFetch) {
console.log('[fetchAndExtractColor] Reusing in-flight fetch for', url);
return existingFetch;
}
console.log('[fetchAndExtractColor] Fetching and extracting color from', url);
const fetchPromise = (async () => {
try {
const response = await fetch(url, {
method: 'GET',
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
});
console.log('[fetchAndExtractColor] Fetch response status:', response.ok, response.status);
if (!response.ok) return undefined;
const data = await response.arrayBuffer();
console.log('[fetchAndExtractColor] Downloaded', data.byteLength, 'bytes');
const color = extractColorFromImage(data);
console.log('[fetchAndExtractColor] Extracted color:', color);
// Don't cache here - fetchAndExtractMetadata will cache full metadata
// This avoids cache collision where color-only fetch overwrites full metadata
return color;
} catch (error) {
console.error('[fetchAndExtractColor] Error fetching/extracting color:', error);
return undefined;
} finally {
inflightColorFetches.delete(inflightKey);
}
})();
inflightColorFetches.set(inflightKey, fetchPromise);
return fetchPromise;
}
/**
@@ -389,27 +422,51 @@ export async function fetchAndExtractMetadata(url: string, accessToken?: string
// Check cache first
const cached = avatarMetadataCache.get(url);
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
console.log('[fetchAndExtractMetadata] Using cached metadata for', url, ':', cached.metadata);
return cached.metadata;
}
try {
const response = await fetch(url, {
method: 'GET',
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
});
if (!response.ok) return {};
const data = await response.arrayBuffer();
const metadata = extractMetadataFromImage(data);
// Cache the result
avatarMetadataCache.set(url, {
metadata,
timestamp: Date.now()
});
return metadata;
} catch {
return {};
// Check if fetch already in progress
const inflightKey = `${url}:${accessToken || ''}`;
const existingFetch = inflightMetadataFetches.get(inflightKey);
if (existingFetch) {
console.log('[fetchAndExtractMetadata] Reusing in-flight fetch for', url);
return existingFetch;
}
console.log('[fetchAndExtractMetadata] Fetching metadata from', url);
const fetchPromise = (async () => {
try {
const response = await fetch(url, {
method: 'GET',
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
});
console.log('[fetchAndExtractMetadata] Fetch response status:', response.ok, response.status);
if (!response.ok) return {};
const data = await response.arrayBuffer();
console.log('[fetchAndExtractMetadata] Downloaded', data.byteLength, 'bytes');
const metadata = extractMetadataFromImage(data);
console.log('[fetchAndExtractMetadata] Extracted metadata:', metadata);
// Cache the result
avatarMetadataCache.set(url, {
metadata,
timestamp: Date.now()
});
return metadata;
} catch {
return {};
} finally {
inflightMetadataFetches.delete(inflightKey);
}
})();
inflightMetadataFetches.set(inflightKey, fetchPromise);
return fetchPromise;
}