File size: 3,241 Bytes
68f7925
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
 * 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),
    };
  }
}