PageSourceSearch

https://rizin.club/js/loading-manager.js

js rizin.club collected 2026-10-05 22:52:38 UTC 20,708 bytes, 566 lines download raw bytes

1// loading-manager.js - プログレッシブローディングを管理するモジュール
2
3/**
4 * プログレッシブローディングを管理するクラス
5 */
6class LoadingManager {
7  constructor() {
8    this.loadingState = {
9      isLoading: true,
10      loadingProgress: 0,
11      isInitialDataLoaded: false,
12      loadingError: null,
13      cachedData: {}
14    };
15    
16    // キャッシュの有効期限(24時間)
17    this.CACHE_EXPIRY = 24 * 60 * 60 * 1000;
18    
19    // ローカルストレージのキー
20    this.STORAGE_KEYS = {
21      EVENT_LIST: 'rizin_event_list',
22      EVENT_DATA_PREFIX: 'rizin_event_data_',
23      LAST_UPDATE: 'rizin_last_update'
24    };
25  }
26
27  /**
28   * ローディング状態を取得
29   * @returns {Object} ローディング状態
30   */
31  getLoadingState() {
32    return this.loadingState;
33  }
34
35  /**
36   * ローディング進捗を更新
37   * @param {number} progress - 進捗率(0-100)
38   */
39  updateProgress(progress) {
40    this.loadingState.loadingProgress = Math.min(Math.max(0, progress), 100);
41  }
42
43  /**
44   * キャッシュからデータを取得
45   * @param {string} key - キャッシュキー
46   * @returns {Object|null} キャッシュされたデータまたはnull
47   */
48  getFromCache(key) {
49    try {
50      const cachedItem = localStorage.getItem(key);
51      if (!cachedItem) return null;
52      
53      const { data, timestamp } = JSON.parse(cachedItem);
54      
55      // キャッシュの有効期限をチェック
56      if (Date.now() - timestamp > this.CACHE_EXPIRY) {
57        localStorage.removeItem(key);
58        return null;
59      }
60      
61      return data;
62    } catch (error) {
63      console.error('キャッシュからの読み込みエラー:', error);
64      return null;
65    }
66  }
67
68  /**
69   * データをキャッシュに保存
70   * @param {string} key - キャッシュキー
71   * @param {Object} data - 保存するデータ
72   */
73  saveToCache(key, data) {
74    try {
75      const cacheItem = {
76        data,
77        timestamp: Date.now()
78      };
79      localStorage.setItem(key, JSON.stringify(cacheItem));
80    } catch (error) {
81      console.error('キャッシュへの保存エラー:', error);
82    }
83  }
84
85  /**
86   * イベントリストを読み込む(キャッシュなし)
87   * @returns {Promise<Array>} イベントリスト
88   */
89  async loadEventList() {
90    this.loadingState.isLoading = true;
91    this.updateProgress(5);
92    
93    try {
94      // event-listは常に最新データを取得(キャッシュしない)
95      console.log('イベントリスト読み込み開始(キャッシュなし)');
96      
97      // キャッシュバスティング用のタイムスタンプを追加
98      const timestamp = Date.now();
99      const response = await fetch(`data/event-list.json?t=${timestamp}`, {
100        cache: 'no-cache',
101        headers: {
102          'Cache-Control': 'no-cache'
103        }
104      });
105      
106      if (!response.ok) {
107        throw new Error(`HTTP error! status: ${response.status}`);
108      }
109      
110      const data = await response.json();
111      console.log('イベントリスト読み込み成功:', data);
112      
113      // イベントリストを処理
114      let eventList = [];
115      
116      if (data.events && Array.isArray(data.events)) {
117        console.log('events配列を検出しました');
118        eventList = data.events.map(event => ({
119          id: event.filename.replace('.json', ''),
120          name: event.name,
121          date: event.date,
122          file: event.filename,
123          venue: event.venue,
124          status: event.status
125        }));
126      } else if (Array.isArray(data)) {
127        console.log('配列形式のイベントリストを検出しました');
128        eventList = data.map(event => ({
129          id: event.file.replace('.json', ''),
130          name: event.name,
131          date: event.date,
132          file: event.file,
133          status: event.status
134        }));
135      } else {
136        console.error('不明なイベントリスト形式:', data);
137        throw new Error('不明なイベントリスト形式');
138      }
139      
140      // イベントリストを日付順に降順ソート
141      eventList.sort((a, b) => {
142        const dateA = new Date(a.date);
143        const dateB = new Date(b.date);
144        return dateB - dateA;
145      });
146      
147      // event-listはキャッシュしない(常に最新データを取得するため)
148      console.log('イベントリスト処理完了:', eventList.length, '件');
149      this.updateProgress(20);
150      
151      return eventList;
152    } catch (error) {
153      console.error('イベントリスト読み込みエラー:', error);
154      this.loadingState.loadingError = 'イベントリストの読み込みに失敗しました';
155      throw error;
156    }
157  }
158
159  /**
160   * 最新のイベントデータを優先的に読み込む(ハイブリッドキャッシュ)
161   * @param {Array} eventList - イベントリスト
162   * @param {number} count - 読み込むイベント数
163   * @returns {Promise<Object>} 読み込んだイベントデータ
164   */
165  async loadPriorityEventData(eventList, count = 3) {
166    const priorityEvents = eventList.slice(0, count);
167    const eventData = {};
168    
169    for (let i = 0; i < priorityEvents.length; i++) {
170      const event = priorityEvents[i];
171      try {
172        // ハイブリッドキャッシュ戦略を使用
173        const shouldUseCache = !this.shouldDisableCacheForEvent(event.date);
174        
175        if (shouldUseCache) {
176          // 古いイベントはキャッシュから取得を試みる
177          const cacheKey = `${this.STORAGE_KEYS.EVENT_DATA_PREFIX}${event.id}`;
178          const cachedData = this.getFromCache(cacheKey);
179          
180          if (cachedData) {
181            console.log(`キャッシュから取得: ${event.file}`);
182            eventData[event.id] = cachedData;
183          } else {
184            // キャッシュになければ新規取得してキャッシュに保存
185            const data = await this.fetchEventData(event.file, false, event.date);
186            eventData[event.id] = data;
187            this.saveToCache(cacheKey, data);
188          }
189        } else {
190          // 最新イベントは常に最新データを取得
191          console.log(`最新データ取得: ${event.file}`);
192          const data = await this.fetchEventData(event.file, false, event.date);
193          eventData[event.id] = data;
194        }
195        
196        // 進捗を更新(20%から50%まで)
197        this.updateProgress(20 + Math.floor((i + 1) / priorityEvents.length * 30));
198      } catch (error) {
199        console.error(`イベントデータ ${event.file} の読み込みに失敗しました:`, error);
200      }
201    }
202    
203    // 初期データ読み込み完了
204    this.loadingState.isInitialDataLoaded = true;
205    
206    return eventData;
207  }
208
209  /**
210   * 残りのイベントデータをバックグラウンドで読み込む(ハイブリッドキャッシュ)
211   * @param {Array} eventList - イベントリスト
212   * @param {number} startIndex - 開始インデックス
213   * @returns {Promise<Object>} 読み込んだイベントデータ
214   */
215  async loadRemainingEventData(eventList, startIndex = 3) {
216    const remainingEvents = eventList.slice(startIndex);
217    const eventData = {};
218    
219    for (let i = 0; i < remainingEvents.length; i++) {
220      const event = remainingEvents[i];
221      try {
222        // ハイブリッドキャッシュ戦略を使用
223        const shouldUseCache = !this.shouldDisableCacheForEvent(event.date);
224        
225        if (shouldUseCache) {
226          // 古いイベントはキャッシュから取得を試みる
227          const cacheKey = `${this.STORAGE_KEYS.EVENT_DATA_PREFIX}${event.id}`;
228          const cachedData = this.getFromCache(cacheKey);
229          
230          if (cachedData) {
231            console.log(`キャッシュから取得: ${event.file}`);
232            eventData[event.id] = cachedData;
233          } else {
234            // キャッシュになければ新規取得してキャッシュに保存
235            const data = await this.fetchEventData(event.file, false, event.date);
236            eventData[event.id] = data;
237            this.saveToCache(cacheKey, data);
238          }
239        } else {
240          // 最新イベントは常に最新データを取得
241          console.log(`最新データ取得: ${event.file}`);
242          const data = await this.fetchEventData(event.file, false, event.date);
243          eventData[event.id] = data;
244        }
245        
246        // 進捗を更新(50%から100%まで)
247        this.updateProgress(50 + Math.floor((i + 1) / remainingEvents.length * 50));
248      } catch (error) {
249        console.error(`イベントデータ ${event.file} の読み込みに失敗しました:`, error);
250      }
251    }
252    
253    // 読み込み完了
254    this.loadingState.isLoading = false;
255    this.updateProgress(100);
256    
257    return eventData;
258  }
259
260  /**
261   * イベントデータを取得
262   * @param {string} filename - イベントファイル名
263   * @param {boolean} forceRefresh - 強制的に最新データを取得するかどうか
264   * @param {string} eventDate - イベント日付(YYYY-MM-DD形式)
265   * @returns {Promise<Object>} イベントデータ
266   */
267  async fetchEventData(filename, forceRefresh = false, eventDate = null) {
268    console.log(`イベントデータ読み込み開始: ${filename}`);
269    
270    // 最新3ヶ月以内のイベントはキャッシュを無効化
271    const shouldDisableCache = this.shouldDisableCacheForEvent(eventDate);
272    const useNoCache = forceRefresh || shouldDisableCache;
273    
274    let url = `data/${filename}`;
275    let fetchOptions = {};
276    
277    if (useNoCache) {
278      console.log(`キャッシュ無効化: ${filename} (${shouldDisableCache ? '最新イベント' : '強制更新'})`);
279      const timestamp = Date.now();
280      url += `?t=${timestamp}`;
281      fetchOptions = {
282        cache: 'no-cache',
283        headers: {
284          'Cache-Control': 'no-cache'
285        }
286      };
287    } else {
288      console.log(`キャッシュ使用: ${filename} (古いイベント)`);
289    }
290    
291    const response = await fetch(url, fetchOptions);
292    
293    if (!response.ok) {
294      throw new Error(`HTTP error! status: ${response.status}`);
295    }
296    
297    const eventData = await response.json();
298    console.log('イベントデータ読み込み成功:', eventData);
299    
300    return eventData;
301  }
302
303  /**
304   * 指定ファイルのLast-Modifiedを取得
305   * @param {string} url
306   * @returns {Promise<string|null>}
307   */
308  async getLastModified(url) {
309    try {
310      console.log('HEADリクエスト開始:', url);
311      const res = await fetch(url, { method: 'HEAD' });
312      console.log('HEADリクエスト結果:', res.status, res.ok);
313      if (res.ok) {
314        const lastModified = res.headers.get('Last-Modified');
315        console.log('Last-Modifiedヘッダー:', lastModified);
316        return lastModified;
317      }
318    } catch (e) {
319      console.warn('HEADリクエスト失敗:', url, e);
320    }
321    return null;
322  }
323
324  /**
325   * 差分取得付きイベントデータ取得
326   * @param {string} eventId
327   * @param {string} filename
328   * @param {string} eventDate
329   * @returns {Promise<Object>} イベントデータ
330   */
331  async fetchEventDataWithDiff(eventId, filename, eventDate = null) {
332    const url = `data/${filename}`;
333    const lastModified = await this.getLastModified(url);
334    const lmKey = `${eventId}_lastModified`;
335    const dataKey = `${eventId}_data`;
336    const prevLastModified = localStorage.getItem(lmKey);
337    let eventData = null;
338    if (lastModified && lastModified === prevLastModified) {
339      // 変更なし: キャッシュ利用
340      const cached = localStorage.getItem(dataKey);
341      if (cached) {
342        try {
343          eventData = JSON.parse(cached);
344          console.log('差分取得: キャッシュ利用', filename);
345          return eventData;
346        } catch (e) {
347          // パース失敗時は再取得
348        }
349      }
350    }
351    // 変更あり or キャッシュなし: 再取得
352    const fetchUrl = lastModified ? `${url}?t=${encodeURIComponent(lastModified)}` : `${url}?t=${Date.now()}`;
353    const response = await fetch(fetchUrl, { cache: 'no-cache', headers: { 'Cache-Control': 'no-cache' } });
354    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
355    eventData = await response.json();
356    if (lastModified) {
357      localStorage.setItem(lmKey, lastModified);
358      localStorage.setItem(dataKey, JSON.stringify(eventData));
359    }
360    console.log('差分取得: 再取得', filename);
361    return eventData;
362  }
363
364  /**
365   * イベントのキャッシュを無効化すべきかどうかを判定
366   * @param {string} eventDate - イベント日付(YYYY-MM-DD形式)
367   * @returns {boolean} キャッシュを無効化すべきかどうか
368   */
369  shouldDisableCacheForEvent(eventDate) {
370    if (!eventDate) return false;
371    
372    try {
373      const eventDateTime = new Date(eventDate);
374      const now = new Date();
375      const threeMonthsAgo = new Date();
376      threeMonthsAgo.setMonth(now.getMonth() - 3);
377      
378      // 3ヶ月以内のイベントはキャッシュを無効化
379      return eventDateTime >= threeMonthsAgo;
380    } catch (error) {
381      console.error('日付解析エラー:', error);
382      return false; // エラーの場合はキャッシュを使用
383    }
384  }
385
386  /**
387   * 特定のイベントデータを読み込む(オンデマンド・ハイブリッドキャッシュ)
388   * @param {string} eventId - イベントID
389   * @param {string} filename - イベントファイル名
390   * @param {boolean} forceRefresh - 強制的に最新データを取得するかどうか
391   * @param {string} eventDate - イベント日付(YYYY-MM-DD形式)
392   * @returns {Promise<Object>} イベントデータ
393   */
394  async loadSpecificEventData(eventId, filename, forceRefresh = false, eventDate = null) {
395    try {
396      if (forceRefresh) {
397        // 強制更新の場合は常に最新データを取得
398        console.log(`強制更新: ${filename}`);
399        const data = await this.fetchEventData(filename, true, eventDate);
400        return data;
401      }
402      
403      // ハイブリッドキャッシュ戦略を使用
404      const shouldUseCache = !this.shouldDisableCacheForEvent(eventDate);
405      
406      if (shouldUseCache) {
407        // 古いイベントはキャッシュから取得を試みる
408        const cacheKey = `${this.STORAGE_KEYS.EVENT_DATA_PREFIX}${eventId}`;
409        const cachedData = this.getFromCache(cacheKey);
410        
411        if (cachedData) {
412          console.log(`キャッシュから取得: ${filename}`);
413          return cachedData;
414        }
415        
416        // キャッシュになければ新規取得してキャッシュに保存
417        const data = await this.fetchEventData(filename, false, eventDate);
418        this.saveToCache(cacheKey, data);
419        return data;
420      } else {
421        // 最新イベントは常に最新データを取得
422        console.log(`最新データ取得: ${filename}`);
423        const data = await this.fetchEventData(filename, false, eventDate);
424        return data;
425      }
426    } catch (error) {
427      console.error(`イベントデータ ${filename} の読み込みに失敗しました:`, error);
428      throw error;
429    }
430  }
431
432  /**
433   * キャッシュをクリア(event-listは除く)
434   */
435  clearCache() {
436    try {
437      // イベントデータのキャッシュのみクリア(event-listはキャッシュしていないのでスキップ)
438      Object.values(this.STORAGE_KEYS).forEach(key => {
439        if (key === this.STORAGE_KEYS.EVENT_LIST) {
440          // event-listはキャッシュしていないのでスキップ
441          return;
442        }
443        
444        if (key.includes('_PREFIX')) {
445          // プレフィックスの場合は関連するすべてのキーを削除
446          const prefix = key;
447          for (let i = 0; i < localStorage.length; i++) {
448            const storageKey = localStorage.key(i);
449            if (storageKey && storageKey.startsWith(prefix)) {
450              localStorage.removeItem(storageKey);
451            }
452          }
453        } else {
454          localStorage.removeItem(key);
455        }
456      });
457      console.log('イベントデータキャッシュをクリアしました(event-listは対象外)');
458    } catch (error) {
459      console.error('キャッシュクリアエラー:', error);
460    }
461  }
462
463  /**
464   * 全データを強制更新
465   * @returns {Promise<Object>} 更新されたデータ
466   */
467  async forceRefreshAllData() {
468    try {
469      console.log('全データの強制更新を開始します');
470      
471      // キャッシュをクリア
472      this.clearCache();
473      
474      // ローディング状態をリセット
475      this.loadingState.isLoading = true;
476      this.loadingState.loadingProgress = 0;
477      this.loadingState.isInitialDataLoaded = false;
478      this.loadingState.loadingError = null;
479      
480      // イベントリストを再読み込み
481      const eventList = await this.loadEventList();
482      
483      // 優先イベントデータを強制更新で読み込み
484      const priorityEventData = await this.loadPriorityEventDataWithRefresh(eventList, 3);
485      
486      // 残りのイベントデータをバックグラウンドで強制更新
487      setTimeout(async () => {
488        await this.loadRemainingEventDataWithRefresh(eventList, 3);
489      }, 100);
490      
491      console.log('全データの強制更新が完了しました');
492      
493      return {
494        eventList,
495        eventData: priorityEventData
496      };
497    } catch (error) {
498      console.error('全データの強制更新に失敗しました:', error);
499      this.loadingState.loadingError = '全データの更新に失敗しました';
500      throw error;
501    }
502  }
503
504  /**
505   * 最新のイベントデータを強制更新で読み込む
506   * @param {Array} eventList - イベントリスト
507   * @param {number} count - 読み込むイベント数
508   * @returns {Promise<Object>} 読み込んだイベントデータ
509   */
510  async loadPriorityEventDataWithRefresh(eventList, count = 3) {
511    const priorityEvents = eventList.slice(0, count);
512    const eventData = {};
513    
514    for (let i = 0; i < priorityEvents.length; i++) {
515      const event = priorityEvents[i];
516      try {
517        // 強制再取得: lastModified無視
518        const url = `data/${event.file}`;
519        const fetchUrl = `${url}?t=${Date.now()}`;
520        const response = await fetch(fetchUrl, { cache: 'no-cache', headers: { 'Cache-Control': 'no-cache' } });
521        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
522        const data = await response.json();
523        eventData[event.id] = data;
524        this.saveToCache(`${this.STORAGE_KEYS.EVENT_DATA_PREFIX}${event.id}`, data);
525        this.updateProgress(20 + Math.floor((i + 1) / priorityEvents.length * 30));
526      } catch (error) {
527        console.error(`イベントデータ ${event.file} の強制更新に失敗しました:`, error);
528      }
529    }
530    this.loadingState.isInitialDataLoaded = true;
531    return eventData;
532  }
533
534  /**
535   * 残りのイベントデータを強制更新でバックグラウンド読み込み
536   * @param {Array} eventList - イベントリスト
537   * @param {number} startIndex - 開始インデックス
538   * @returns {Promise<Object>} 読み込んだイベントデータ
539   */
540  async loadRemainingEventDataWithRefresh(eventList, startIndex = 3) {
541    const remainingEvents = eventList.slice(startIndex);
542    const eventData = {};
543    for (let i = 0; i < remainingEvents.length; i++) {
544      const event = remainingEvents[i];
545      try {
546        // 強制再取得: lastModified無視
547        const url = `data/${event.file}`;
548        const fetchUrl = `${url}?t=${Date.now()}`;
549        const response = await fetch(fetchUrl, { cache: 'no-cache', headers: { 'Cache-Control': 'no-cache' } });
550        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
551        const data = await response.json();
552        eventData[event.id] = data;
553        this.saveToCache(`${this.STORAGE_KEYS.EVENT_DATA_PREFIX}${event.id}`, data);
554        this.updateProgress(50 + Math.floor((i + 1) / remainingEvents.length * 50));
555      } catch (error) {
556        console.error(`イベントデータ ${event.file} の強制更新に失敗しました:`, error);
557      }
558    }
559    this.loadingState.isLoading = false;
560    this.updateProgress(100);
561    return eventData;
562  }
563}
564
565// グローバルインスタンスをエクスポート
566const loadingManager = new LoadingManager();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.