@@ -12,6 +12,14 @@ export interface LyricCacheOptions {
1212 * 缓存过期时间(毫秒)
1313 */
1414 expiration ?: number
15+ /**
16+ * IndexedDB数据库名称
17+ */
18+ dbName ?: string
19+ /**
20+ * IndexedDB存储对象名称
21+ */
22+ storeName ?: string
1523}
1624
1725/**
@@ -29,10 +37,57 @@ interface CacheData {
2937export class LyricCache {
3038 private prefix : string
3139 private expiration : number
40+ private dbName : string
41+ private storeName : string
42+ private db : IDBDatabase | null = null
43+ private dbReady : Promise < void > | null = null
3244
3345 constructor ( options : LyricCacheOptions = { } ) {
3446 this . prefix = options . prefix || 'lyric_cache_'
3547 this . expiration = options . expiration || 7 * 24 * 60 * 60 * 1000 // 默认7天
48+ this . dbName = options . dbName || 'lyric_cache_db'
49+ this . storeName = options . storeName || 'lyrics'
50+ this . initDB ( )
51+ }
52+
53+ /**
54+ * 初始化IndexedDB
55+ */
56+ private initDB ( ) : void {
57+ if ( typeof window === 'undefined' || ! ( 'indexedDB' in window ) ) {
58+ console . warn ( 'IndexedDB is not supported in this environment' )
59+ return
60+ }
61+
62+ this . dbReady = new Promise < void > ( ( resolve ) => {
63+ const request = window . indexedDB . open ( this . dbName , 1 )
64+
65+ request . onerror = ( ) => {
66+ console . error ( 'Error opening IndexedDB:' , request . error )
67+ resolve ( ) // 即使失败也继续,回退到localStorage
68+ }
69+
70+ request . onsuccess = ( ) => {
71+ this . db = request . result
72+ resolve ( )
73+ }
74+
75+ request . onupgradeneeded = ( event ) => {
76+ const db = ( event . target as IDBOpenDBRequest ) . result
77+ if ( ! db . objectStoreNames . contains ( this . storeName ) ) {
78+ db . createObjectStore ( this . storeName , { keyPath : 'key' } )
79+ }
80+ }
81+ } )
82+ }
83+
84+ /**
85+ * 确保DB已初始化
86+ */
87+ private ensureDBInitialized ( ) : void {
88+ if ( ! this . dbReady ) {
89+ this . initDB ( )
90+ }
3691 }
3792
3893 /**
@@ -55,20 +110,155 @@ export class LyricCache {
55110 return now - data . timestamp > expirationTime
56111 }
57112
113+ /**
114+ * 检查localStorage是否有足够空间
115+ * @param data 要存储的数据
116+ * @returns 是否有足够空间
117+ */
118+ private hasLocalStorageSpace ( data : string ) : boolean {
119+ if ( typeof window === 'undefined' || ! ( 'localStorage' in window ) ) {
120+ return false
121+ }
122+
123+ try {
124+ const testKey = `${ this . prefix } test`
125+ window . localStorage . setItem ( testKey , data )
126+ window . localStorage . removeItem ( testKey )
127+ return true
128+ } catch ( error ) {
129+ return false
130+ }
131+ }
132+
133+ /**
134+ * 使用IndexedDB存储数据
135+ * @param key 缓存键
136+ * @param data 缓存数据
137+ */
138+ private async setToIndexedDB ( key : string , data : CacheData ) : Promise < boolean > {
139+ this . ensureDBInitialized ( )
140+ if ( ! this . dbReady ) return false
141+
142+ await this . dbReady
143+ if ( ! this . db ) return false
144+
145+ const db = this . db
146+ return new Promise < boolean > ( ( resolve ) => {
147+ const transaction = db . transaction ( this . storeName , 'readwrite' )
148+ const store = transaction . objectStore ( this . storeName )
149+ const request = store . put ( { key, ...data } )
150+
151+ request . onsuccess = ( ) => resolve ( true )
152+ request . onerror = ( ) => {
153+ console . error ( 'Error storing data in IndexedDB:' , request . error )
154+ resolve ( false )
155+ }
156+ } )
157+ }
158+
159+ /**
160+ * 从IndexedDB获取数据
161+ * @param key 缓存键
162+ * @returns 缓存数据或null
163+ */
164+ private async getFromIndexedDB ( key : string ) : Promise < BaseLyricItem [ ] | null > {
165+ this . ensureDBInitialized ( )
166+ if ( ! this . dbReady ) return null
167+
168+ await this . dbReady
169+ if ( ! this . db ) return null
170+
171+ const db = this . db
172+ return new Promise < BaseLyricItem [ ] | null > ( ( resolve ) => {
173+ const transaction = db . transaction ( this . storeName , 'readonly' )
174+ const store = transaction . objectStore ( this . storeName )
175+ const request = store . get ( key )
176+
177+ request . onsuccess = ( ) => {
178+ const data = request . result
179+ if ( ! data ) {
180+ resolve ( null )
181+ return
182+ }
183+
184+ if ( this . isExpired ( data ) ) {
185+ this . removeFromIndexedDB ( key )
186+ resolve ( null )
187+ return
188+ }
189+
190+ resolve ( data . data )
191+ }
192+
193+ request . onerror = ( ) => {
194+ console . error ( 'Error getting data from IndexedDB:' , request . error )
195+ resolve ( null )
196+ }
197+ } )
198+ }
199+
200+ /**
201+ * 从IndexedDB移除数据
202+ * @param key 缓存键
203+ */
204+ private async removeFromIndexedDB ( key : string ) : Promise < void > {
205+ this . ensureDBInitialized ( )
206+ if ( ! this . dbReady ) return
207+
208+ await this . dbReady
209+ if ( ! this . db ) return
210+
211+ const db = this . db
212+ const transaction = db . transaction ( this . storeName , 'readwrite' )
213+ const store = transaction . objectStore ( this . storeName )
214+ store . delete ( key )
215+ }
216+
217+ /**
218+ * 清除IndexedDB中的所有数据
219+ */
220+ private async clearIndexedDB ( ) : Promise < void > {
221+ this . ensureDBInitialized ( )
222+ if ( ! this . dbReady ) return
223+
224+ await this . dbReady
225+ if ( ! this . db ) return
226+
227+ const db = this . db
228+ const transaction = db . transaction ( this . storeName , 'readwrite' )
229+ const store = transaction . objectStore ( this . storeName )
230+ store . clear ( )
231+ }
232+
58233 /**
59234 * 缓存歌词
60235 * @param key 缓存键
61236 * @param data 歌词数据
62237 */
63- set ( key : string , data : BaseLyricItem [ ] ) : void {
238+ async set ( key : string , data : BaseLyricItem [ ] ) : Promise < void > {
64239 try {
65240 const cacheKey = this . generateKey ( key )
66241 const cacheData : CacheData = {
67242 data,
68243 timestamp : Date . now ( ) ,
69244 expiration : this . expiration ,
70245 }
71- localStorage . setItem ( cacheKey , JSON . stringify ( cacheData ) )
246+
247+ const jsonData = JSON . stringify ( cacheData )
248+
249+ // 尝试使用localStorage
250+ if ( this . hasLocalStorageSpace ( jsonData ) ) {
251+ window . localStorage . setItem ( cacheKey , jsonData )
252+ return
253+ }
254+
255+ // localStorage空间不足,尝试使用IndexedDB
256+ const storedInIndexedDB = await this . setToIndexedDB ( cacheKey , cacheData )
257+ if ( storedInIndexedDB ) {
258+ return
259+ }
260+
261+ console . warn ( 'Failed to cache lyric in both localStorage and IndexedDB' )
72262 } catch ( error ) {
73263 console . error ( 'Error caching lyric:' , error )
74264 }
@@ -79,19 +269,29 @@ export class LyricCache {
79269 * @param key 缓存键
80270 * @returns 缓存的歌词数据,或null如果不存在或已过期
81271 */
82- get ( key : string ) : BaseLyricItem [ ] | null {
272+ async get ( key : string ) : Promise < BaseLyricItem [ ] | null > {
83273 try {
84274 const cacheKey = this . generateKey ( key )
85- const cachedData = localStorage . getItem ( cacheKey )
86- if ( ! cachedData ) {
87- return null
275+
276+ // 尝试从localStorage获取
277+ if ( typeof window !== 'undefined' && 'localStorage' in window ) {
278+ const cachedData = window . localStorage . getItem ( cacheKey )
279+ if ( cachedData ) {
280+ const parsedData : CacheData = JSON . parse ( cachedData )
281+ if ( ! this . isExpired ( parsedData ) ) {
282+ return parsedData . data
283+ }
284+ this . remove ( key )
285+ }
88286 }
89- const parsedData : CacheData = JSON . parse ( cachedData )
90- if ( this . isExpired ( parsedData ) ) {
91- this . remove ( key )
92- return null
287+
288+ // 从localStorage获取失败,尝试从IndexedDB获取
289+ const indexedDBData = await this . getFromIndexedDB ( cacheKey )
290+ if ( indexedDBData ) {
291+ return indexedDBData
93292 }
94- return parsedData . data
293+
294+ return null
95295 } catch ( error ) {
96296 console . error ( 'Error getting cached lyric:' , error )
97297 return null
@@ -102,10 +302,13 @@ export class LyricCache {
102302 * 移除缓存的歌词
103303 * @param key 缓存键
104304 */
105- remove ( key : string ) : void {
305+ async remove ( key : string ) : Promise < void > {
106306 try {
107307 const cacheKey = this . generateKey ( key )
108- localStorage . removeItem ( cacheKey )
308+ if ( typeof window !== 'undefined' && 'localStorage' in window ) {
309+ window . localStorage . removeItem ( cacheKey )
310+ }
311+ await this . removeFromIndexedDB ( cacheKey )
109312 } catch ( error ) {
110313 console . error ( 'Error removing cached lyric:' , error )
111314 }
@@ -114,14 +317,20 @@ export class LyricCache {
114317 /**
115318 * 清除所有缓存的歌词
116319 */
117- clear ( ) : void {
320+ async clear ( ) : Promise < void > {
118321 try {
119- const keys = Object . keys ( localStorage )
120- keys . forEach ( ( key ) => {
121- if ( key . startsWith ( this . prefix ) ) {
122- localStorage . removeItem ( key )
123- }
124- } )
322+ // 清除localStorage中的缓存
323+ if ( typeof window !== 'undefined' && 'localStorage' in window ) {
324+ const keys = Object . keys ( window . localStorage )
325+ keys . forEach ( ( key ) => {
326+ if ( key . startsWith ( this . prefix ) ) {
327+ window . localStorage . removeItem ( key )
328+ }
329+ } )
330+ }
331+
332+ // 清除IndexedDB中的缓存
333+ await this . clearIndexedDB ( )
125334 } catch ( error ) {
126335 console . error ( 'Error clearing cached lyrics:' , error )
127336 }
@@ -132,8 +341,9 @@ export class LyricCache {
132341 * @param key 缓存键
133342 * @returns 是否存在有效缓存
134343 */
135- has ( key : string ) : boolean {
136- return this . get ( key ) !== null
344+ async has ( key : string ) : Promise < boolean > {
345+ const data = await this . get ( key )
346+ return data !== null
137347 }
138348}
139349
0 commit comments