a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: improve img cache

intergrav 850c82a5 37ab96c7

+34 -13
+34 -13
src/js/image-cache.js
··· 12 12 this.db = null; 13 13 } 14 14 15 + // extract cache key from URL query parameters 15 16 getKey(url) { 16 17 const urlObj = new URL(url); 17 18 return `${urlObj.searchParams.get("id")}:${urlObj.searchParams.get("size")}`; 18 19 } 19 20 20 - async initDB() { 21 - if (this.db) return; 21 + // initialize and return IndexedDB reference 22 + async ensureDB() { 23 + if (this.db) return this.db; 22 24 try { 23 25 await new Promise((resolve) => { 24 26 const req = indexedDB.open("tinysubCache", 1); ··· 35 37 req.onerror = () => resolve(); 36 38 }); 37 39 } catch (error) { 38 - // IndexedDB not available 40 + console.error("[ImageCache]", "IndexedDB initialization failed:", error); 39 41 } 40 - } 41 - 42 - async ensureDB() { 43 - if (!this.db) await this.initDB(); 44 42 return this.db; 45 43 } 46 44 45 + // retrieve blob URL from IndexedDB 47 46 async getFromDB(key) { 48 47 const db = await this.ensureDB(); 49 48 if (!db) return null; ··· 56 55 const result = req.result; 57 56 if (result && result.blob) { 58 57 const blobUrl = URL.createObjectURL(result.blob); 58 + const oldUrl = this.cache.get(key); 59 + if (oldUrl) URL.revokeObjectURL(oldUrl); 59 60 this.cache.set(key, blobUrl); 60 61 resolve(blobUrl); 61 62 } else { ··· 69 70 }); 70 71 } 71 72 73 + // store blob in IndexedDB 72 74 async putInDB(key, blob) { 73 75 const db = await this.ensureDB(); 74 76 if (!db) return; ··· 80 82 req.onsuccess = () => resolve(); 81 83 req.onerror = () => resolve(); 82 84 } catch (error) { 85 + console.error("[ImageCache]", "Failed to store blob in DB:", error); 83 86 resolve(); 84 87 } 85 88 }); 86 89 } 87 90 91 + // clear all cached images from IndexedDB 88 92 async clearDB() { 89 93 if (!this.db) return; 90 94 try { ··· 95 99 req.onsuccess = () => resolve(); 96 100 req.onerror = () => resolve(); 97 101 }); 98 - } catch (error) {} 102 + } catch (error) { 103 + console.error("[ImageCache]", "Failed to clear DB:", error); 104 + } 99 105 } 100 106 107 + // get cached image or queue for fetching 101 108 async get(url) { 102 109 const key = this.getKey(url); 103 110 if (this.cache.has(key)) return this.cache.get(key); ··· 116 123 return promise; 117 124 } 118 125 126 + // process pending fetch requests with concurrency control 119 127 async processQueue() { 120 128 while (this.activeCount < this.maxConcurrent && this.queue.length > 0) { 121 129 const { url, key, resolve } = this.queue.shift(); 122 130 this.activeCount++; 123 - resolve(await this.fetch(url, key)); 124 - this.activeCount--; 131 + try { 132 + resolve(await this.fetch(url, key)); 133 + } finally { 134 + this.activeCount--; 135 + } 125 136 } 126 137 } 127 138 139 + // fetch image from network and cache it 128 140 async fetch(url, key) { 129 141 try { 130 142 const r = await fetch(url); 131 143 if (!r.ok) throw new Error(`HTTP ${r.status}`); 132 144 const blob = await r.blob(); 133 145 const blobUrl = URL.createObjectURL(blob); 146 + 147 + const oldUrl = this.cache.get(key); 148 + if (oldUrl) URL.revokeObjectURL(oldUrl); 149 + 134 150 this.cache.set(key, blobUrl); 151 + 135 152 this.putInDB(key, blob); 136 153 return blobUrl; 137 154 } catch (error) { ··· 140 157 } 141 158 } 142 159 143 - clear() { 160 + // clear all cache and stop observing 161 + async clear() { 144 162 this.cache.forEach((blobUrl) => URL.revokeObjectURL(blobUrl)); 145 163 this.cache.clear(); 146 164 this.failed.clear(); ··· 150 168 this.pendingImages.clear(); 151 169 if (this.observer) this.observer.disconnect(); 152 170 this.observer = null; 153 - this.clearDB(); 171 + await this.clearDB(); 154 172 } 155 173 174 + // initialize IntersectionObserver for lazy-loading 156 175 initObserver() { 157 176 if (this.observer) return; 158 177 this.observer = new IntersectionObserver( ··· 168 187 } 169 188 }); 170 189 }, 171 - { rootMargin: "50px" }, 190 + { rootMargin: "48px" }, 172 191 ); 173 192 } 174 193 194 + // queue image for lazy-loading when it enters viewport 175 195 observeImage(img, loadFn) { 176 196 this.initObserver(); 177 197 this.pendingImages.set(img, loadFn); 178 198 this.observer.observe(img); 179 199 } 180 200 201 + // check if all URLs are cached; returns array or null 181 202 cachedUrls(urls) { 182 203 const cached = urls.map((url) => this.cache.get(this.getKey(url))); 183 204 return cached.every((c) => c) ? cached : null;