feat: implement LRU caching for user colors and avatar metadata to optimize performance
This commit is contained in:
@@ -128,6 +128,10 @@ function PinnedMessage({
|
|||||||
}, [room, eventId, mx])
|
}, [room, eventId, mx])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sender = pinnedEvent?.getSender();
|
||||||
|
const senderAvatarMxc = sender ? getMemberAvatarMxc(room, sender) : undefined;
|
||||||
|
const customUserColor = useOtherUserColor(sender ?? '', senderAvatarMxc);
|
||||||
|
|
||||||
const handleOpenClick: MouseEventHandler = (evt) => {
|
const handleOpenClick: MouseEventHandler = (evt) => {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
const evtId = evt.currentTarget.getAttribute('data-event-id');
|
const evtId = evt.currentTarget.getAttribute('data-event-id');
|
||||||
@@ -175,14 +179,9 @@ function PinnedMessage({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
const sender = pinnedEvent.getSender()!;
|
const displayName = getMemberDisplayName(room, sender!) ?? getMxIdLocalPart(sender!) ?? sender!;
|
||||||
const displayName = getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender;
|
|
||||||
const senderAvatarMxc = getMemberAvatarMxc(room, sender);
|
|
||||||
const getContent = (() => pinnedEvent.getContent()) as GetContentCallback;
|
const getContent = (() => pinnedEvent.getContent()) as GetContentCallback;
|
||||||
|
|
||||||
// Get custom user color from avatar metadata
|
|
||||||
const customUserColor = useOtherUserColor(sender, senderAvatarMxc);
|
|
||||||
|
|
||||||
const memberPowerTag = getMemberPowerTag(sender);
|
const memberPowerTag = getMemberPowerTag(sender);
|
||||||
const tagColor = memberPowerTag?.color
|
const tagColor = memberPowerTag?.color
|
||||||
? accessibleTagColors?.get(memberPowerTag.color)
|
? accessibleTagColors?.get(memberPowerTag.color)
|
||||||
|
|||||||
@@ -204,9 +204,52 @@ export function useUserColor(): [
|
|||||||
return [color, updateColor, loading];
|
return [color, updateColor, loading];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache for user colors to avoid refetching for each message
|
/**
|
||||||
const userColorCache = new Map<string, { color: string | undefined; timestamp: number }>();
|
* LRU Cache implementation with max size limit
|
||||||
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
*/
|
||||||
|
class LRUCache<K, V> {
|
||||||
|
private cache: Map<K, V>;
|
||||||
|
|
||||||
|
private maxSize: number;
|
||||||
|
|
||||||
|
constructor(maxSize: number) {
|
||||||
|
this.cache = new Map();
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K): V | undefined {
|
||||||
|
const value = this.cache.get(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
// Move to end (most recently used)
|
||||||
|
this.cache.delete(key);
|
||||||
|
this.cache.set(key, value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V): void {
|
||||||
|
// If key exists, delete it first to re-add at end
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
// If at max size, remove oldest (first) entry
|
||||||
|
else if (this.cache.size >= this.maxSize) {
|
||||||
|
const firstKey = this.cache.keys().next().value;
|
||||||
|
if (firstKey !== undefined) {
|
||||||
|
this.cache.delete(firstKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LRU cache for user colors to avoid refetching for each message
|
||||||
|
const userColorCache = new LRUCache<string, { color: string | undefined; timestamp: number }>(100);
|
||||||
|
const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to get another user's chosen profile color from their avatar metadata
|
* Hook to get another user's chosen profile color from their avatar metadata
|
||||||
|
|||||||
@@ -18,6 +18,46 @@ import { embedColorInWebP, extractColorFromWebP, removeColorFromWebP } from './w
|
|||||||
import { embedColorInJPEG, extractColorFromJPEG, removeColorFromJPEG } from './jpegMetadata';
|
import { embedColorInJPEG, extractColorFromJPEG, removeColorFromJPEG } from './jpegMetadata';
|
||||||
import { embedColorInGIF, extractColorFromGIF, removeColorFromGIF } from './gifMetadata';
|
import { embedColorInGIF, extractColorFromGIF, removeColorFromGIF } from './gifMetadata';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LRU Cache implementation with max size limit
|
||||||
|
*/
|
||||||
|
class LRUCache<K, V> {
|
||||||
|
private cache: Map<K, V>;
|
||||||
|
|
||||||
|
private maxSize: number;
|
||||||
|
|
||||||
|
constructor(maxSize: number) {
|
||||||
|
this.cache = new Map();
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K): V | undefined {
|
||||||
|
const value = this.cache.get(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
this.cache.set(key, value);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V): void {
|
||||||
|
if (this.cache.has(key)) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
else if (this.cache.size >= this.maxSize) {
|
||||||
|
const firstKey = this.cache.keys().next().value;
|
||||||
|
if (firstKey !== undefined) {
|
||||||
|
this.cache.delete(firstKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.cache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gradient configuration for profile styling
|
* Gradient configuration for profile styling
|
||||||
* All colors support RGBA format (e.g., #FF550080 for 50% opacity)
|
* All colors support RGBA format (e.g., #FF550080 for 50% opacity)
|
||||||
@@ -300,6 +340,10 @@ export function getExtension(format: ImageFormat): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LRU cache for avatar metadata to avoid refetching
|
||||||
|
const avatarMetadataCache = new LRUCache<string, { metadata: ImageMetadata; timestamp: number }>(100);
|
||||||
|
const METADATA_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch and extract color from an image URL (supports all formats)
|
* Fetch and extract color from an image URL (supports all formats)
|
||||||
* @param url - HTTP URL to fetch the image from
|
* @param url - HTTP URL to fetch the image from
|
||||||
@@ -307,6 +351,12 @@ export function getExtension(format: ImageFormat): string {
|
|||||||
* @returns The color string if found, or undefined
|
* @returns The color string if found, or undefined
|
||||||
*/
|
*/
|
||||||
export async function fetchAndExtractColor(url: string, accessToken?: string | null): Promise<string | undefined> {
|
export async function fetchAndExtractColor(url: string, accessToken?: string | null): Promise<string | undefined> {
|
||||||
|
// Check if full metadata is cached
|
||||||
|
const cached = avatarMetadataCache.get(url);
|
||||||
|
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
||||||
|
return cached.metadata.color;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -315,7 +365,15 @@ export async function fetchAndExtractColor(url: string, accessToken?: string | n
|
|||||||
if (!response.ok) return undefined;
|
if (!response.ok) return undefined;
|
||||||
|
|
||||||
const data = await response.arrayBuffer();
|
const data = await response.arrayBuffer();
|
||||||
return extractColorFromImage(data);
|
const color = extractColorFromImage(data);
|
||||||
|
|
||||||
|
// Cache the color result as part of metadata
|
||||||
|
avatarMetadataCache.set(url, {
|
||||||
|
metadata: { color },
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
return color;
|
||||||
} catch {
|
} catch {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -328,6 +386,12 @@ export async function fetchAndExtractColor(url: string, accessToken?: string | n
|
|||||||
* @returns Object with color and banner if found
|
* @returns Object with color and banner if found
|
||||||
*/
|
*/
|
||||||
export async function fetchAndExtractMetadata(url: string, accessToken?: string | null): Promise<ImageMetadata> {
|
export async function fetchAndExtractMetadata(url: string, accessToken?: string | null): Promise<ImageMetadata> {
|
||||||
|
// Check cache first
|
||||||
|
const cached = avatarMetadataCache.get(url);
|
||||||
|
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
||||||
|
return cached.metadata;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -336,7 +400,15 @@ export async function fetchAndExtractMetadata(url: string, accessToken?: string
|
|||||||
if (!response.ok) return {};
|
if (!response.ok) return {};
|
||||||
|
|
||||||
const data = await response.arrayBuffer();
|
const data = await response.arrayBuffer();
|
||||||
return extractMetadataFromImage(data);
|
const metadata = extractMetadataFromImage(data);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
avatarMetadataCache.set(url, {
|
||||||
|
metadata,
|
||||||
|
timestamp: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
return metadata;
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user