File size: 8,751 Bytes
494c9e4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/**
 * 本地 Demo 缓存 - 浏览器内持久化存储
 * 用于在浏览器中持久化本地加载的 demo 数据,支持刷新后恢复
 */

import type { AnalysisData } from '../api/GLTR_API';
import type { IDemoStorage, SaveOptions, SaveResult, LoadResult } from './demoStorage';
import { ensureJsonExtension } from '../utils/localFileUtils';
import { extractErrorMessage } from '../utils/errorUtils';
import { hashContent, CryptoSubtleUnavailableError } from '../utils/hashUtils';

const DB_NAME = 'InfoRadarDB';
const DB_VERSION = 2;
const STORE_NAME = 'demos';

/**
 * 本地 Demo 缓存实现
 * 使用 IndexedDB 持久化本地 demo 数据,支持刷新后恢复
 */
export class LocalDemoCache implements IDemoStorage {
    readonly type = 'local' as const;
    private dbPromise: Promise<IDBDatabase> | null = null;

    /**
     * 检查 IndexedDB 是否可用
     */
    static isAvailable(): boolean {
        return typeof indexedDB !== 'undefined';
    }

    /**
     * 初始化或获取数据库连接
     */
    private async getDB(): Promise<IDBDatabase> {
        if (!LocalDemoCache.isAvailable()) {
            throw new Error('IndexedDB 不可用,可能是浏览器不支持或处于隐私模式');
        }

        if (this.dbPromise) {
            return this.dbPromise;
        }

        this.dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
            const request = indexedDB.open(DB_NAME, DB_VERSION);

            request.onerror = () => {
                reject(new Error('Failed to open IndexedDB'));
            };

            request.onsuccess = () => {
                resolve(request.result);
            };

            request.onupgradeneeded = (event) => {
                const db = (event.target as IDBOpenDBRequest).result;
                
                // 删除旧的对象存储(如果存在)
                if (db.objectStoreNames.contains(STORE_NAME)) {
                    db.deleteObjectStore(STORE_NAME);
                }
                
                // 创建新的对象存储,使用 hash 作为主键
                db.createObjectStore(STORE_NAME, { keyPath: 'key' });
            };
        });

        return this.dbPromise;
    }

    /**
     * 保存 demo 到缓存
     * @param data 要保存的数据
     * @param options 保存选项(符合 IDemoStorage 接口)
     */
    async save(data: AnalysisData, options: SaveOptions): Promise<SaveResult> {
        try {
            // 内部计算内容哈希
            const hash = await hashContent(data);
            
            const db = await this.getDB();
            const transaction = db.transaction([STORE_NAME], 'readwrite');
            const store = transaction.objectStore(STORE_NAME);

            const filename = ensureJsonExtension(options.name);
            const key = `${filename}~${hash}`; // 使用 filename~hash 作为 key

            const record = {
                key,
                filename,
                data,
                timestamp: Date.now()
            };

            const request = store.put(record);

            return new Promise((resolve) => {
                request.onsuccess = () => {
                    resolve({
                        success: true,
                        message: 'Saved to local cache',
                        file: filename,
                        hash // 返回计算好的哈希值
                    });
                };

                request.onerror = () => {
                    const error = request.error;
                    if (error && error.name === 'QuotaExceededError') {
                        resolve({
                            success: false,
                            message: 'Storage quota exceeded, please clear cache and try again'
                        });
                    } else {
                        resolve({
                            success: false,
                            message: 'Failed to save to cache'
                        });
                    }
                };
            });
        } catch (error) {
            // CryptoSubtleUnavailableError 需要特殊处理,直接重新抛出让调用方处理
            if (error instanceof CryptoSubtleUnavailableError) {
                throw error;
            }
            if (error instanceof DOMException && error.name === 'QuotaExceededError') {
                return {
                    success: false,
                    message: 'Storage quota exceeded, please clear cache and try again'
                };
            }
            return {
                success: false,
                message: extractErrorMessage(error, 'Save failed')
            };
        }
    }

    /**
     * 从缓存加载 demo
     * @param key 完整的 key,格式为 "filename~hash"
     */
    async load(key?: string): Promise<LoadResult> {
        if (!key) {
            return { success: false, message: 'Key is missing' };
        }

        try {
            const db = await this.getDB();
            const transaction = db.transaction([STORE_NAME], 'readonly');
            const store = transaction.objectStore(STORE_NAME);

            const request = store.get(key);

            return new Promise((resolve) => {
                request.onsuccess = () => {
                    const record = request.result;
                    
                    if (!record || !record.data) {
                        resolve({
                            success: false,
                            message: 'File not found in local cache, please open again'
                        });
                        return;
                    }

                    resolve({
                        success: true,
                        data: record.data as AnalysisData
                    });
                };

                request.onerror = () => {
                    const error = request.error;
                    console.error('从缓存读取失败:', error);
                    resolve({
                        success: false,
                        message: 'Failed to read from cache'
                    });
                };
            });
        } catch (error) {
            return {
                success: false,
                message: extractErrorMessage(error, '加载失败')
            };
        }
    }

    /**
     * 删除指定的 demo
     * @param key 完整的 key,格式为 "filename~hash"
     */
    async delete(key: string): Promise<boolean> {
        try {
            const db = await this.getDB();
            const transaction = db.transaction([STORE_NAME], 'readwrite');
            const store = transaction.objectStore(STORE_NAME);

            const request = store.delete(key);

            return new Promise((resolve) => {
                request.onsuccess = () => resolve(true);
                request.onerror = () => {
                    console.error('删除失败:', request.error);
                    resolve(false);
                };
            });
        } catch (error) {
            console.error('删除 demo 失败:', error);
            return false;
        }
    }

    /**
     * 清空所有本地缓存
     */
    async clear(): Promise<boolean> {
        try {
            const db = await this.getDB();
            const transaction = db.transaction([STORE_NAME], 'readwrite');
            const store = transaction.objectStore(STORE_NAME);

            const request = store.clear();

            return new Promise((resolve) => {
                request.onsuccess = () => resolve(true);
                request.onerror = () => {
                    console.error('清空失败:', request.error);
                    resolve(false);
                };
            });
        } catch (error) {
            console.error('清空缓存失败:', error);
            return false;
        }
    }

    /**
     * 列出所有已缓存的 demo
     */
    async list(): Promise<string[]> {
        try {
            const db = await this.getDB();
            const transaction = db.transaction([STORE_NAME], 'readonly');
            const store = transaction.objectStore(STORE_NAME);

            const request = store.getAllKeys();

            return new Promise((resolve) => {
                request.onsuccess = () => {
                    resolve(request.result as string[]);
                };
                request.onerror = () => {
                    console.error('列出缓存失败:', request.error);
                    resolve([]);
                };
            });
        } catch (error) {
            console.error('列出缓存失败:', error);
            return [];
        }
    }
}