FE_Test / server /utils /image-memory-cache.ts
GitHub Actions
Deploy from GitHub Actions [test] - 2025-10-31 10:18:25
5f2aab6
/**
* HuggingFace Spaces環境での画像メモリキャッシュ
* ファイルシステム書き込み制限を回避するためメモリ内で管理
*/
export class ImageMemoryCache {
private static instance: ImageMemoryCache;
private cache = new Map<string, { buffer: Buffer; contentType: string; createdAt: number }>();
private maxSize = 100 * 1024 * 1024; // 100MB制限
private maxAge = 30 * 60 * 1000; // 30分
private currentSize = 0;
private constructor() {
// 定期的に古いエントリを削除
setInterval(() => this.cleanup(), 5 * 60 * 1000); // 5分ごと
}
static getInstance(): ImageMemoryCache {
if (!ImageMemoryCache.instance) {
ImageMemoryCache.instance = new ImageMemoryCache();
}
return ImageMemoryCache.instance;
}
/**
* 画像をキャッシュに保存
*/
store(id: string, buffer: Buffer, contentType: string = 'image/webp'): string {
const size = buffer.length;
// サイズ制限チェック
if (this.currentSize + size > this.maxSize) {
this.evictOldest();
}
this.cache.set(id, {
buffer,
contentType,
createdAt: Date.now(),
});
this.currentSize += size;
console.log(`[ImageCache] Stored ${id} (${(size / 1024).toFixed(1)}KB), total: ${(this.currentSize / 1024 / 1024).toFixed(1)}MB`);
// APIエンドポイント経由のURL返却
return `/api/cached-images/${id}`;
}
/**
* 画像を取得
*/
get(id: string): { buffer: Buffer; contentType: string } | null {
const entry = this.cache.get(id);
if (!entry) return null;
// 有効期限チェック
if (Date.now() - entry.createdAt > this.maxAge) {
this.remove(id);
return null;
}
return {
buffer: entry.buffer,
contentType: entry.contentType,
};
}
/**
* 画像を削除
*/
private remove(id: string): void {
const entry = this.cache.get(id);
if (entry) {
this.currentSize -= entry.buffer.length;
this.cache.delete(id);
}
}
/**
* 最古のエントリを削除
*/
private evictOldest(): void {
let oldest: { id: string; createdAt: number } | null = null;
for (const [id, entry] of this.cache.entries()) {
if (!oldest || entry.createdAt < oldest.createdAt) {
oldest = { id, createdAt: entry.createdAt };
}
}
if (oldest) {
console.log(`[ImageCache] Evicting oldest entry: ${oldest.id}`);
this.remove(oldest.id);
}
}
/**
* 期限切れエントリをクリーンアップ
*/
private cleanup(): void {
const now = Date.now();
const expired: string[] = [];
for (const [id, entry] of this.cache.entries()) {
if (now - entry.createdAt > this.maxAge) {
expired.push(id);
}
}
if (expired.length > 0) {
console.log(`[ImageCache] Cleaning up ${expired.length} expired entries`);
expired.forEach((id) => this.remove(id));
}
}
/**
* キャッシュ状態を取得
*/
getStats() {
return {
entries: this.cache.size,
sizeBytes: this.currentSize,
sizeMB: (this.currentSize / 1024 / 1024).toFixed(2),
maxSizeMB: (this.maxSize / 1024 / 1024).toFixed(2),
};
}
}