|
1 | | -// HACK: moving global variables from index.html to here for loadRemote |
2 | | - |
3 | | -let dbVersion = 1 |
4 | | -let dbName = 'whisper.ggerganov.com'; |
5 | | -let indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB |
6 | | - |
7 | | -// fetch a remote file from remote URL using the Fetch API |
8 | | -async function fetchRemote(url, cbProgress, cbPrint) { |
9 | | - cbPrint('fetchRemote: downloading with fetch()...'); |
10 | | - |
11 | | - const response = await fetch( |
12 | | - url, |
13 | | - { |
14 | | - method: 'GET', |
15 | | - } |
16 | | - ); |
17 | | - |
18 | | - if (!response.ok) { |
19 | | - cbPrint('fetchRemote: failed to fetch ' + url); |
20 | | - return; |
21 | | - } |
22 | | - |
23 | | - const contentLength = response.headers.get('content-length'); |
24 | | - const total = parseInt(contentLength, 10); |
25 | | - const reader = response.body.getReader(); |
26 | | - |
27 | | - var chunks = []; |
28 | | - var receivedLength = 0; |
29 | | - var progressLast = -1; |
30 | | - |
31 | | - while (true) { |
32 | | - const { done, value } = await reader.read(); |
33 | | - |
34 | | - if (done) { |
35 | | - break; |
36 | | - } |
37 | | - |
38 | | - chunks.push(value); |
39 | | - receivedLength += value.length; |
40 | | - |
41 | | - if (contentLength) { |
42 | | - cbProgress(receivedLength/total); |
43 | | - |
44 | | - var progressCur = Math.round((receivedLength / total) * 10); |
45 | | - if (progressCur != progressLast) { |
46 | | - cbPrint('fetchRemote: fetching ' + 10*progressCur + '% ...'); |
47 | | - progressLast = progressCur; |
48 | | - } |
49 | | - } |
50 | | - } |
| 1 | +// src/components/api/whisper/indexedDB.js |
| 2 | +// Silent IndexedDB caching loader with multi-URL fallbacks. |
| 3 | + |
| 4 | +var dbVersion = 1; |
| 5 | +var dbName = 'whisper.ggerganov.com'; |
| 6 | +var indexedDB = |
| 7 | + (window.indexedDB || |
| 8 | + window.mozIndexedDB || |
| 9 | + window.webkitIndexedDB || |
| 10 | + window.msIndexedDB); |
| 11 | + |
| 12 | +// fetch a URL as a Uint8Array (progress callback optional) |
| 13 | +async function fetchBinary(url, cbProgress, cbPrint) { |
| 14 | + cbPrint && cbPrint('fetchBinary: GET ' + url); |
| 15 | + |
| 16 | + const res = await fetch(url, { method: 'GET', cache: 'no-cache' }); |
| 17 | + if (!res.ok) throw new Error('http-' + res.status); |
| 18 | + |
| 19 | + // No stream? Just read all at once. |
| 20 | + if (!res.body || !res.body.getReader) { |
| 21 | + const buf = new Uint8Array(await res.arrayBuffer()); |
| 22 | + cbProgress && cbProgress(1); |
| 23 | + return buf; |
| 24 | + } |
| 25 | + |
| 26 | + const total = parseInt(res.headers.get('content-length') || '0', 10) || 0; |
| 27 | + const reader = res.body.getReader(); |
| 28 | + |
| 29 | + let received = 0; |
| 30 | + const chunks = []; |
| 31 | + while (true) { |
| 32 | + const { done, value } = await reader.read(); |
| 33 | + if (done) break; |
| 34 | + chunks.push(value); |
| 35 | + received += value.length; |
| 36 | + if (total && cbProgress) cbProgress(received / total); |
| 37 | + } |
| 38 | + |
| 39 | + const out = new Uint8Array(received); |
| 40 | + let pos = 0; |
| 41 | + for (const c of chunks) { out.set(c, pos); pos += c.length; } |
| 42 | + if (!total && cbProgress) cbProgress(1); |
| 43 | + return out; |
| 44 | +} |
51 | 45 |
|
52 | | - var position = 0; |
53 | | - var chunksAll = new Uint8Array(receivedLength); |
| 46 | +function openDB() { |
| 47 | + return new Promise((resolve, reject) => { |
| 48 | + const rq = indexedDB.open(dbName, dbVersion); |
| 49 | + rq.onupgradeneeded = (ev) => { |
| 50 | + const db = ev.target.result; |
| 51 | + if (!db.objectStoreNames.contains('models')) { |
| 52 | + db.createObjectStore('models', { autoIncrement: false }); |
| 53 | + } |
| 54 | + }; |
| 55 | + rq.onsuccess = () => resolve(rq.result); |
| 56 | + rq.onerror = () => reject(new Error('idb-open')); |
| 57 | + rq.onblocked = () => reject(new Error('idb-blocked')); |
| 58 | + rq.onabort = () => reject(new Error('idb-abort')); |
| 59 | + }); |
| 60 | +} |
54 | 61 |
|
55 | | - for (var chunk of chunks) { |
56 | | - chunksAll.set(chunk, position); |
57 | | - position += chunk.length; |
58 | | - } |
| 62 | +async function getCached(db, key) { |
| 63 | + try { |
| 64 | + return await new Promise((resolve) => { |
| 65 | + const tx = db.transaction(['models'], 'readonly'); |
| 66 | + const os = tx.objectStore('models'); |
| 67 | + const g = os.get(key); |
| 68 | + g.onsuccess = () => { |
| 69 | + let v = g.result; |
| 70 | + if (v && v instanceof ArrayBuffer) v = new Uint8Array(v); |
| 71 | + resolve(v || null); |
| 72 | + }; |
| 73 | + g.onerror = () => resolve(null); |
| 74 | + }); |
| 75 | + } catch { return null; } |
| 76 | +} |
59 | 77 |
|
60 | | - return chunksAll; |
| 78 | +async function putCached(db, key, data, cbPrint) { |
| 79 | + try { |
| 80 | + await new Promise((resolve) => { |
| 81 | + const tx = db.transaction(['models'], 'readwrite'); |
| 82 | + const os = tx.objectStore('models'); |
| 83 | + const p = os.put(data, key); |
| 84 | + p.onsuccess = () => { cbPrint && cbPrint('IDB: stored ' + key); resolve(); }; |
| 85 | + p.onerror = () => resolve(); |
| 86 | + }); |
| 87 | + } catch (e) { cbPrint && cbPrint('IDB store error: ' + e); } |
61 | 88 | } |
62 | 89 |
|
63 | | -// load remote data |
64 | | -// - check if the data is already in the IndexedDB |
65 | | -// - if not, fetch it from the remote URL and store it in the IndexedDB |
66 | | -export function loadRemote(url, dst, size_mb, cbProgress, cbReady, cbCancel, cbPrint) { |
67 | | - if (!navigator.storage || !navigator.storage.estimate) { |
68 | | - cbPrint('loadRemote: navigator.storage.estimate() is not supported'); |
69 | | - } else { |
70 | | - // query the storage quota and print it |
71 | | - navigator.storage.estimate().then(function (estimate) { |
72 | | - cbPrint('loadRemote: storage quota: ' + estimate.quota + ' bytes'); |
73 | | - cbPrint('loadRemote: storage usage: ' + estimate.usage + ' bytes'); |
74 | | - }); |
| 90 | +// MAIN: try urls in order; cache successful one under that exact url key. |
| 91 | +export async function loadRemoteWithFallbacks(urls, dst, sizeMB, cbProgress, cbReady, cbCancel, cbPrint) { |
| 92 | + try { |
| 93 | + if (navigator.storage && navigator.storage.estimate) { |
| 94 | + const est = await navigator.storage.estimate(); |
| 95 | + cbPrint && cbPrint(`IDB quota ~${est.quota}, used ~${est.usage}`); |
75 | 96 | } |
76 | | - |
77 | | - // check if the data is already in the IndexedDB |
78 | | - var rq = indexedDB.open(dbName, dbVersion); |
79 | | - |
80 | | - rq.onupgradeneeded = function (event) { |
81 | | - var db = event.target.result; |
82 | | - if (db.version == 1) { |
83 | | - var os = db.createObjectStore('models', { autoIncrement: false }); |
84 | | - cbPrint('loadRemote: created IndexedDB ' + db.name + ' version ' + db.version); |
85 | | - } else { |
86 | | - // clear the database |
87 | | - var os = event.currentTarget.transaction.objectStore('models'); |
88 | | - os.clear(); |
89 | | - cbPrint('loadRemote: cleared IndexedDB ' + db.name + ' version ' + db.version); |
| 97 | + } catch {} |
| 98 | + |
| 99 | + let db = null; |
| 100 | + try { db = await openDB(); } catch { db = null; } |
| 101 | + |
| 102 | + const tinyBlob = (b) => !b || (b.length || 0) < 4096; |
| 103 | + const urlList = (Array.isArray(urls) ? urls : [urls]).filter(Boolean); |
| 104 | + |
| 105 | + if (urlList.length === 0) { |
| 106 | + cbCancel && cbCancel(); |
| 107 | + return; |
| 108 | + } |
| 109 | + |
| 110 | + for (const url of urlList) { |
| 111 | + try { |
| 112 | + // cache hit? |
| 113 | + if (db) { |
| 114 | + const cached = await getCached(db, url); |
| 115 | + if (cached && !tinyBlob(cached)) { |
| 116 | + cbReady(dst, cached); |
| 117 | + return; |
90 | 118 | } |
91 | | - }; |
92 | | - |
93 | | - rq.onsuccess = function (event) { |
94 | | - var db = event.target.result; |
95 | | - var tx = db.transaction(['models'], 'readonly'); |
96 | | - var os = tx.objectStore('models'); |
97 | | - var rq = os.get(url); |
98 | | - |
99 | | - rq.onsuccess = function (event) { |
100 | | - if (rq.result) { |
101 | | - cbPrint('loadRemote: "' + url + '" is already in the IndexedDB'); |
102 | | - cbReady(dst, rq.result); |
103 | | - } else { |
104 | | - // data is not in the IndexedDB |
105 | | - cbPrint('loadRemote: "' + url + '" is not in the IndexedDB'); |
106 | | - |
107 | | - // alert and ask the user to confirm |
108 | | - if (!confirm( |
109 | | - 'You are about to download ' + size_mb + ' MB of data.\n' + |
110 | | - 'The model data will be cached in the browser for future use.\n\n' + |
111 | | - 'Press OK to continue.')) { |
112 | | - cbCancel(); |
113 | | - return; |
114 | | - } |
115 | | - |
116 | | - fetchRemote(url, cbProgress, cbPrint).then(function (data) { |
117 | | - if (data) { |
118 | | - // store the data in the IndexedDB |
119 | | - var rq = indexedDB.open(dbName, dbVersion); |
120 | | - rq.onsuccess = function (event) { |
121 | | - var db = event.target.result; |
122 | | - var tx = db.transaction(['models'], 'readwrite'); |
123 | | - var os = tx.objectStore('models'); |
124 | | - |
125 | | - var rq = null; |
126 | | - try { |
127 | | - var rq = os.put(data, url); |
128 | | - } catch (e) { |
129 | | - cbPrint('loadRemote: failed to store "' + url + '" in the IndexedDB: \n' + e); |
130 | | - cbCancel(); |
131 | | - return; |
132 | | - } |
133 | | - |
134 | | - rq.onsuccess = function (event) { |
135 | | - cbPrint('loadRemote: "' + url + '" stored in the IndexedDB'); |
136 | | - cbReady(dst, data); |
137 | | - }; |
138 | | - |
139 | | - rq.onerror = function (event) { |
140 | | - cbPrint('loadRemote: failed to store "' + url + '" in the IndexedDB'); |
141 | | - cbCancel(); |
142 | | - }; |
143 | | - }; |
144 | | - } |
145 | | - }); |
146 | | - } |
147 | | - }; |
148 | | - |
149 | | - rq.onerror = function (event) { |
150 | | - cbPrint('loadRemote: failed to get data from the IndexedDB'); |
151 | | - cbCancel(); |
152 | | - }; |
153 | | - }; |
154 | | - |
155 | | - rq.onerror = function (event) { |
156 | | - cbPrint('loadRemote: failed to open IndexedDB'); |
157 | | - cbCancel(); |
158 | | - }; |
159 | | - |
160 | | - rq.onblocked = function (event) { |
161 | | - cbPrint('loadRemote: failed to open IndexedDB: blocked'); |
162 | | - cbCancel(); |
163 | | - }; |
| 119 | + } |
| 120 | + |
| 121 | + cbPrint && cbPrint(`loadRemote: fetching ~${sizeMB} MB from ${url}`); |
| 122 | + const data = await fetchBinary(url, cbProgress, cbPrint); |
| 123 | + if (tinyBlob(data)) throw new Error('tiny-blob'); |
| 124 | + if (db) await putCached(db, url, data, cbPrint); |
| 125 | + cbReady(dst, data); |
| 126 | + return; |
| 127 | + } catch (e) { |
| 128 | + cbPrint && cbPrint(`loadRemote: failed on ${url} (${e}), trying next...`); |
| 129 | + } |
| 130 | + } |
164 | 131 |
|
165 | | - rq.onabort = function (event) { |
166 | | - cbPrint('loadRemote: failed to open IndexedDB: abort'); |
167 | | - cbCancel(); |
168 | | - }; |
| 132 | + cbCancel(); |
169 | 133 | } |
0 commit comments