@runanywhere/web 0.1.0-beta.6 → 0.1.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/dist/Foundation/ErrorTypes.d.ts +1 -0
  2. package/dist/Foundation/ErrorTypes.d.ts.map +1 -1
  3. package/dist/Foundation/ErrorTypes.js +3 -0
  4. package/dist/Foundation/ErrorTypes.js.map +1 -1
  5. package/dist/Foundation/EventBus.d.ts +0 -1
  6. package/dist/Foundation/EventBus.d.ts.map +1 -1
  7. package/dist/Foundation/StructOffsets.d.ts +5 -37
  8. package/dist/Foundation/StructOffsets.d.ts.map +1 -1
  9. package/dist/Foundation/StructOffsets.js +6 -157
  10. package/dist/Foundation/StructOffsets.js.map +1 -1
  11. package/dist/Foundation/WASMBridge.d.ts +8 -236
  12. package/dist/Foundation/WASMBridge.d.ts.map +1 -1
  13. package/dist/Foundation/WASMBridge.js +7 -388
  14. package/dist/Foundation/WASMBridge.js.map +1 -1
  15. package/dist/Infrastructure/DeviceCapabilities.d.ts.map +1 -1
  16. package/dist/Infrastructure/DeviceCapabilities.js +1 -3
  17. package/dist/Infrastructure/DeviceCapabilities.js.map +1 -1
  18. package/dist/Infrastructure/ExtensionPoint.d.ts +114 -0
  19. package/dist/Infrastructure/ExtensionPoint.d.ts.map +1 -0
  20. package/dist/Infrastructure/ExtensionPoint.js +178 -0
  21. package/dist/Infrastructure/ExtensionPoint.js.map +1 -0
  22. package/dist/Infrastructure/LocalFileStorage.d.ts +134 -0
  23. package/dist/Infrastructure/LocalFileStorage.d.ts.map +1 -0
  24. package/dist/Infrastructure/LocalFileStorage.js +428 -0
  25. package/dist/Infrastructure/LocalFileStorage.js.map +1 -0
  26. package/dist/Infrastructure/ModelDownloader.d.ts +21 -5
  27. package/dist/Infrastructure/ModelDownloader.d.ts.map +1 -1
  28. package/dist/Infrastructure/ModelDownloader.js +79 -7
  29. package/dist/Infrastructure/ModelDownloader.js.map +1 -1
  30. package/dist/Infrastructure/ModelFileInference.d.ts +39 -0
  31. package/dist/Infrastructure/ModelFileInference.d.ts.map +1 -0
  32. package/dist/Infrastructure/ModelFileInference.js +119 -0
  33. package/dist/Infrastructure/ModelFileInference.js.map +1 -0
  34. package/dist/Infrastructure/ModelLoaderTypes.d.ts +91 -12
  35. package/dist/Infrastructure/ModelLoaderTypes.d.ts.map +1 -1
  36. package/dist/Infrastructure/ModelLoaderTypes.js +7 -1
  37. package/dist/Infrastructure/ModelLoaderTypes.js.map +1 -1
  38. package/dist/Infrastructure/ModelManager.d.ts +31 -104
  39. package/dist/Infrastructure/ModelManager.d.ts.map +1 -1
  40. package/dist/Infrastructure/ModelManager.js +207 -568
  41. package/dist/Infrastructure/ModelManager.js.map +1 -1
  42. package/dist/Infrastructure/ModelRegistry.d.ts +6 -8
  43. package/dist/Infrastructure/ModelRegistry.d.ts.map +1 -1
  44. package/dist/Infrastructure/ModelRegistry.js +11 -4
  45. package/dist/Infrastructure/ModelRegistry.js.map +1 -1
  46. package/dist/Infrastructure/OPFSStorage.d.ts +8 -0
  47. package/dist/Infrastructure/OPFSStorage.d.ts.map +1 -1
  48. package/dist/Infrastructure/OPFSStorage.js +37 -0
  49. package/dist/Infrastructure/OPFSStorage.js.map +1 -1
  50. package/dist/Public/Extensions/RunAnywhere+ModelManagement.d.ts +12 -4
  51. package/dist/Public/Extensions/RunAnywhere+ModelManagement.d.ts.map +1 -1
  52. package/dist/Public/Extensions/RunAnywhere+ModelManagement.js +23 -51
  53. package/dist/Public/Extensions/RunAnywhere+ModelManagement.js.map +1 -1
  54. package/dist/Public/Extensions/RunAnywhere+VoiceAgent.d.ts +42 -10
  55. package/dist/Public/Extensions/RunAnywhere+VoiceAgent.d.ts.map +1 -1
  56. package/dist/Public/Extensions/RunAnywhere+VoiceAgent.js +63 -161
  57. package/dist/Public/Extensions/RunAnywhere+VoiceAgent.js.map +1 -1
  58. package/dist/Public/Extensions/RunAnywhere+VoicePipeline.d.ts +3 -29
  59. package/dist/Public/Extensions/RunAnywhere+VoicePipeline.d.ts.map +1 -1
  60. package/dist/Public/Extensions/RunAnywhere+VoicePipeline.js +26 -42
  61. package/dist/Public/Extensions/RunAnywhere+VoicePipeline.js.map +1 -1
  62. package/dist/Public/Extensions/VoicePipelineTypes.d.ts +28 -37
  63. package/dist/Public/Extensions/VoicePipelineTypes.d.ts.map +1 -1
  64. package/dist/Public/Extensions/VoicePipelineTypes.js +4 -1
  65. package/dist/Public/Extensions/VoicePipelineTypes.js.map +1 -1
  66. package/dist/Public/RunAnywhere.d.ts +29 -85
  67. package/dist/Public/RunAnywhere.d.ts.map +1 -1
  68. package/dist/Public/RunAnywhere.js +169 -211
  69. package/dist/Public/RunAnywhere.js.map +1 -1
  70. package/dist/index.d.ts +19 -39
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +15 -31
  73. package/dist/index.js.map +1 -1
  74. package/dist/types/index.d.ts +0 -1
  75. package/dist/types/index.d.ts.map +1 -1
  76. package/dist/types/index.js +1 -0
  77. package/dist/types/index.js.map +1 -1
  78. package/package.json +4 -10
  79. package/dist/Foundation/PlatformAdapter.d.ts +0 -101
  80. package/dist/Foundation/PlatformAdapter.d.ts.map +0 -1
  81. package/dist/Foundation/PlatformAdapter.js +0 -417
  82. package/dist/Foundation/PlatformAdapter.js.map +0 -1
  83. package/dist/Foundation/SherpaONNXBridge.d.ts +0 -147
  84. package/dist/Foundation/SherpaONNXBridge.d.ts.map +0 -1
  85. package/dist/Foundation/SherpaONNXBridge.js +0 -345
  86. package/dist/Foundation/SherpaONNXBridge.js.map +0 -1
  87. package/dist/Infrastructure/AudioCapture.d.ts +0 -99
  88. package/dist/Infrastructure/AudioCapture.d.ts.map +0 -1
  89. package/dist/Infrastructure/AudioCapture.js +0 -264
  90. package/dist/Infrastructure/AudioCapture.js.map +0 -1
  91. package/dist/Infrastructure/AudioPlayback.d.ts +0 -53
  92. package/dist/Infrastructure/AudioPlayback.d.ts.map +0 -1
  93. package/dist/Infrastructure/AudioPlayback.js +0 -117
  94. package/dist/Infrastructure/AudioPlayback.js.map +0 -1
  95. package/dist/Infrastructure/VLMWorkerBridge.d.ts +0 -211
  96. package/dist/Infrastructure/VLMWorkerBridge.d.ts.map +0 -1
  97. package/dist/Infrastructure/VLMWorkerBridge.js +0 -264
  98. package/dist/Infrastructure/VLMWorkerBridge.js.map +0 -1
  99. package/dist/Infrastructure/VLMWorkerRuntime.d.ts +0 -38
  100. package/dist/Infrastructure/VLMWorkerRuntime.d.ts.map +0 -1
  101. package/dist/Infrastructure/VLMWorkerRuntime.js +0 -503
  102. package/dist/Infrastructure/VLMWorkerRuntime.js.map +0 -1
  103. package/dist/Infrastructure/VideoCapture.d.ts +0 -130
  104. package/dist/Infrastructure/VideoCapture.d.ts.map +0 -1
  105. package/dist/Infrastructure/VideoCapture.js +0 -236
  106. package/dist/Infrastructure/VideoCapture.js.map +0 -1
  107. package/dist/Public/Extensions/DiffusionTypes.d.ts +0 -64
  108. package/dist/Public/Extensions/DiffusionTypes.d.ts.map +0 -1
  109. package/dist/Public/Extensions/DiffusionTypes.js +0 -28
  110. package/dist/Public/Extensions/DiffusionTypes.js.map +0 -1
  111. package/dist/Public/Extensions/EmbeddingsTypes.d.ts +0 -33
  112. package/dist/Public/Extensions/EmbeddingsTypes.d.ts.map +0 -1
  113. package/dist/Public/Extensions/EmbeddingsTypes.js +0 -13
  114. package/dist/Public/Extensions/EmbeddingsTypes.js.map +0 -1
  115. package/dist/Public/Extensions/RunAnywhere+Diffusion.d.ts +0 -44
  116. package/dist/Public/Extensions/RunAnywhere+Diffusion.d.ts.map +0 -1
  117. package/dist/Public/Extensions/RunAnywhere+Diffusion.js +0 -189
  118. package/dist/Public/Extensions/RunAnywhere+Diffusion.js.map +0 -1
  119. package/dist/Public/Extensions/RunAnywhere+Embeddings.d.ts +0 -56
  120. package/dist/Public/Extensions/RunAnywhere+Embeddings.d.ts.map +0 -1
  121. package/dist/Public/Extensions/RunAnywhere+Embeddings.js +0 -240
  122. package/dist/Public/Extensions/RunAnywhere+Embeddings.js.map +0 -1
  123. package/dist/Public/Extensions/RunAnywhere+STT.d.ts +0 -97
  124. package/dist/Public/Extensions/RunAnywhere+STT.d.ts.map +0 -1
  125. package/dist/Public/Extensions/RunAnywhere+STT.js +0 -417
  126. package/dist/Public/Extensions/RunAnywhere+STT.js.map +0 -1
  127. package/dist/Public/Extensions/RunAnywhere+StructuredOutput.d.ts +0 -69
  128. package/dist/Public/Extensions/RunAnywhere+StructuredOutput.d.ts.map +0 -1
  129. package/dist/Public/Extensions/RunAnywhere+StructuredOutput.js +0 -196
  130. package/dist/Public/Extensions/RunAnywhere+StructuredOutput.js.map +0 -1
  131. package/dist/Public/Extensions/RunAnywhere+TTS.d.ts +0 -55
  132. package/dist/Public/Extensions/RunAnywhere+TTS.d.ts.map +0 -1
  133. package/dist/Public/Extensions/RunAnywhere+TTS.js +0 -253
  134. package/dist/Public/Extensions/RunAnywhere+TTS.js.map +0 -1
  135. package/dist/Public/Extensions/RunAnywhere+TextGeneration.d.ts +0 -80
  136. package/dist/Public/Extensions/RunAnywhere+TextGeneration.d.ts.map +0 -1
  137. package/dist/Public/Extensions/RunAnywhere+TextGeneration.js +0 -470
  138. package/dist/Public/Extensions/RunAnywhere+TextGeneration.js.map +0 -1
  139. package/dist/Public/Extensions/RunAnywhere+ToolCalling.d.ts +0 -82
  140. package/dist/Public/Extensions/RunAnywhere+ToolCalling.d.ts.map +0 -1
  141. package/dist/Public/Extensions/RunAnywhere+ToolCalling.js +0 -576
  142. package/dist/Public/Extensions/RunAnywhere+ToolCalling.js.map +0 -1
  143. package/dist/Public/Extensions/RunAnywhere+VAD.d.ts +0 -70
  144. package/dist/Public/Extensions/RunAnywhere+VAD.d.ts.map +0 -1
  145. package/dist/Public/Extensions/RunAnywhere+VAD.js +0 -231
  146. package/dist/Public/Extensions/RunAnywhere+VAD.js.map +0 -1
  147. package/dist/Public/Extensions/RunAnywhere+VLM.d.ts +0 -58
  148. package/dist/Public/Extensions/RunAnywhere+VLM.d.ts.map +0 -1
  149. package/dist/Public/Extensions/RunAnywhere+VLM.js +0 -262
  150. package/dist/Public/Extensions/RunAnywhere+VLM.js.map +0 -1
  151. package/dist/Public/Extensions/STTTypes.d.ts +0 -61
  152. package/dist/Public/Extensions/STTTypes.d.ts.map +0 -1
  153. package/dist/Public/Extensions/STTTypes.js +0 -16
  154. package/dist/Public/Extensions/STTTypes.js.map +0 -1
  155. package/dist/Public/Extensions/TTSTypes.d.ts +0 -31
  156. package/dist/Public/Extensions/TTSTypes.d.ts.map +0 -1
  157. package/dist/Public/Extensions/TTSTypes.js +0 -3
  158. package/dist/Public/Extensions/TTSTypes.js.map +0 -1
  159. package/dist/Public/Extensions/ToolCallingTypes.d.ts +0 -78
  160. package/dist/Public/Extensions/ToolCallingTypes.d.ts.map +0 -1
  161. package/dist/Public/Extensions/ToolCallingTypes.js +0 -8
  162. package/dist/Public/Extensions/ToolCallingTypes.js.map +0 -1
  163. package/dist/Public/Extensions/VADTypes.d.ts +0 -30
  164. package/dist/Public/Extensions/VADTypes.d.ts.map +0 -1
  165. package/dist/Public/Extensions/VADTypes.js +0 -8
  166. package/dist/Public/Extensions/VADTypes.js.map +0 -1
  167. package/dist/Public/Extensions/VLMTypes.d.ts +0 -56
  168. package/dist/Public/Extensions/VLMTypes.d.ts.map +0 -1
  169. package/dist/Public/Extensions/VLMTypes.js +0 -24
  170. package/dist/Public/Extensions/VLMTypes.js.map +0 -1
  171. package/dist/types/LLMTypes.d.ts +0 -48
  172. package/dist/types/LLMTypes.d.ts.map +0 -1
  173. package/dist/types/LLMTypes.js +0 -8
  174. package/dist/types/LLMTypes.js.map +0 -1
  175. package/dist/workers/vlm-worker.d.ts +0 -9
  176. package/dist/workers/vlm-worker.d.ts.map +0 -1
  177. package/dist/workers/vlm-worker.js +0 -10
  178. package/dist/workers/vlm-worker.js.map +0 -1
  179. package/wasm/racommons-webgpu.js +0 -156
  180. package/wasm/racommons-webgpu.wasm +0 -0
  181. package/wasm/racommons.js +0 -126
  182. package/wasm/racommons.wasm +0 -0
  183. package/wasm/sherpa/sherpa-onnx-asr.js +0 -1538
  184. package/wasm/sherpa/sherpa-onnx-glue-original.js +0 -19
  185. package/wasm/sherpa/sherpa-onnx-glue.js +0 -17
  186. package/wasm/sherpa/sherpa-onnx-tts.js +0 -657
  187. package/wasm/sherpa/sherpa-onnx-vad.js +0 -337
  188. package/wasm/sherpa/sherpa-onnx-wave.js +0 -88
  189. package/wasm/sherpa/sherpa-onnx.wasm +0 -0
@@ -0,0 +1,428 @@
1
+ /**
2
+ * LocalFileStorage - Persistent model storage using the File System Access API
3
+ *
4
+ * Allows users to choose a real folder on their local filesystem for model
5
+ * storage. Models are saved as actual files (e.g. ~/ai-models/smollm2.gguf)
6
+ * that persist permanently — no browser eviction, no re-downloads.
7
+ *
8
+ * The directory handle is stored in IndexedDB (not cookies — handles are
9
+ * structured-cloneable objects that can't be serialized to strings).
10
+ *
11
+ * Browser support:
12
+ * - Chrome 122+: Full support with persistent permissions ("Allow on every visit")
13
+ * - Chrome 86-121: Supported but re-prompts for permission each session
14
+ * - Edge: Same as Chrome (Chromium-based)
15
+ * - Firefox/Safari: NOT supported — falls back to OPFS storage
16
+ *
17
+ * Usage:
18
+ * const storage = new LocalFileStorage();
19
+ * await storage.chooseDirectory(); // User picks a folder
20
+ * await storage.saveModel('model-id', data);
21
+ *
22
+ * // On return visit:
23
+ * const restored = await storage.restoreDirectory();
24
+ * if (restored) {
25
+ * const data = await storage.loadModel('model-id');
26
+ * }
27
+ */
28
+ import { SDKLogger } from '../Foundation/SDKLogger';
29
+ const logger = new SDKLogger('LocalFileStorage');
30
+ /* eslint-enable @typescript-eslint/no-explicit-any */
31
+ // ---------------------------------------------------------------------------
32
+ // IndexedDB Constants
33
+ // ---------------------------------------------------------------------------
34
+ const DB_NAME = 'runanywhere-storage';
35
+ const DB_VERSION = 1;
36
+ const STORE_NAME = 'handles';
37
+ const HANDLE_KEY = 'modelDirectory';
38
+ const LS_DIR_NAME_KEY = 'runanywhere_storage_dir_name';
39
+ // ---------------------------------------------------------------------------
40
+ // LocalFileStorage
41
+ // ---------------------------------------------------------------------------
42
+ export class LocalFileStorage {
43
+ dirHandle = null;
44
+ _isReady = false;
45
+ _hasStoredHandle = false;
46
+ /** Per-key write lock to prevent concurrent writes to the same file. */
47
+ writeLocks = new Map();
48
+ // -------------------------------------------------------------------------
49
+ // Static
50
+ // -------------------------------------------------------------------------
51
+ /** Whether the File System Access API is available in this browser. */
52
+ static get isSupported() {
53
+ return typeof window !== 'undefined' && 'showDirectoryPicker' in window;
54
+ }
55
+ /**
56
+ * Get the stored directory name from localStorage (fast, synchronous).
57
+ * Available immediately on page load before IndexedDB restores the handle.
58
+ * Returns the folder name only (e.g. "ai-models"), not the full path.
59
+ */
60
+ static get storedDirectoryName() {
61
+ try {
62
+ return localStorage.getItem(LS_DIR_NAME_KEY);
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ // -------------------------------------------------------------------------
69
+ // State
70
+ // -------------------------------------------------------------------------
71
+ /** Whether the storage is ready for use (directory selected + permission granted). */
72
+ get isReady() {
73
+ return this._isReady && this.dirHandle !== null;
74
+ }
75
+ /** Whether a handle was found in IndexedDB (even if permission isn't granted yet). */
76
+ get hasStoredHandle() {
77
+ return this._hasStoredHandle;
78
+ }
79
+ /** The name of the chosen directory (for display in UI). */
80
+ get directoryName() {
81
+ return this.dirHandle?.name ?? null;
82
+ }
83
+ // -------------------------------------------------------------------------
84
+ // Directory Selection
85
+ // -------------------------------------------------------------------------
86
+ /**
87
+ * Prompt the user to choose a directory for model storage.
88
+ * Opens the OS folder picker dialog.
89
+ * Stores the directory handle in IndexedDB for future sessions.
90
+ *
91
+ * @returns true if a directory was selected, false if cancelled
92
+ */
93
+ async chooseDirectory() {
94
+ if (!LocalFileStorage.isSupported) {
95
+ logger.warning('File System Access API not supported in this browser');
96
+ return false;
97
+ }
98
+ try {
99
+ // showDirectoryPicker requires user gesture (button click)
100
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
101
+ this.dirHandle = await window.showDirectoryPicker({
102
+ mode: 'readwrite',
103
+ });
104
+ await this.storeHandle(this.dirHandle);
105
+ this._isReady = true;
106
+ this._hasStoredHandle = true;
107
+ // Persist directory name in localStorage for fast UI display on next visit
108
+ try {
109
+ localStorage.setItem(LS_DIR_NAME_KEY, this.dirHandle.name);
110
+ }
111
+ catch { /* non-critical */ }
112
+ logger.info(`Local storage directory selected: ${this.dirHandle.name}`);
113
+ return true;
114
+ }
115
+ catch (err) {
116
+ if (err instanceof Error && err.name === 'AbortError') {
117
+ logger.debug('User cancelled directory picker');
118
+ return false;
119
+ }
120
+ const message = err instanceof Error ? err.message : String(err);
121
+ logger.error(`Failed to choose directory: ${message}`);
122
+ return false;
123
+ }
124
+ }
125
+ /**
126
+ * Attempt to restore a previously chosen directory from IndexedDB.
127
+ *
128
+ * On Chrome 122+, if the user selected "Allow on every visit",
129
+ * permission is automatically granted without any prompt.
130
+ *
131
+ * @returns true if directory was restored and permission is granted.
132
+ * false if no handle stored or permission not granted (UI
133
+ * should show a "Re-authorize" button).
134
+ */
135
+ async restoreDirectory() {
136
+ if (!LocalFileStorage.isSupported)
137
+ return false;
138
+ try {
139
+ const handle = await this.retrieveHandle();
140
+ if (!handle) {
141
+ logger.debug('No stored directory handle found');
142
+ return false;
143
+ }
144
+ this._hasStoredHandle = true;
145
+ this.dirHandle = handle;
146
+ // queryPermission does NOT require user gesture
147
+ const permission = await handle.queryPermission({ mode: 'readwrite' });
148
+ if (permission === 'granted') {
149
+ this._isReady = true;
150
+ logger.info(`Local storage restored: ${handle.name}`);
151
+ return true;
152
+ }
153
+ // Permission not granted yet — user needs to click a button
154
+ // to trigger requestPermission() via requestAccess()
155
+ logger.debug(`Local storage handle found but permission is '${permission}' — needs re-authorization`);
156
+ this._isReady = false;
157
+ return false;
158
+ }
159
+ catch (err) {
160
+ const message = err instanceof Error ? err.message : String(err);
161
+ logger.warning(`Failed to restore directory: ${message}`);
162
+ return false;
163
+ }
164
+ }
165
+ /**
166
+ * Request readwrite permission on a previously stored handle.
167
+ * MUST be called from a user gesture (button click handler).
168
+ *
169
+ * @returns true if permission was granted
170
+ */
171
+ async requestAccess() {
172
+ if (!this.dirHandle) {
173
+ logger.warning('No directory handle to request access for');
174
+ return false;
175
+ }
176
+ try {
177
+ const permission = await this.dirHandle.requestPermission({ mode: 'readwrite' });
178
+ if (permission === 'granted') {
179
+ this._isReady = true;
180
+ logger.info(`Local storage re-authorized: ${this.dirHandle.name}`);
181
+ return true;
182
+ }
183
+ logger.debug(`Permission request result: ${permission}`);
184
+ return false;
185
+ }
186
+ catch (err) {
187
+ const message = err instanceof Error ? err.message : String(err);
188
+ logger.warning(`Permission request failed: ${message}`);
189
+ return false;
190
+ }
191
+ }
192
+ // -------------------------------------------------------------------------
193
+ // Model Operations
194
+ // -------------------------------------------------------------------------
195
+ /**
196
+ * Save model data to the local filesystem.
197
+ * Uses a per-key lock to prevent concurrent writes from corrupting files.
198
+ * @param key - Model identifier (used as filename)
199
+ * @param data - Model file data
200
+ */
201
+ async saveModel(key, data) {
202
+ return this.withWriteLock(key, () => this._saveModelImpl(key, data));
203
+ }
204
+ /**
205
+ * Save model data from a ReadableStream to the local filesystem.
206
+ * Streams data directly to disk without buffering the entire file in memory.
207
+ * Uses a per-key lock to prevent concurrent writes from corrupting files.
208
+ * @param key - Model identifier (used as filename)
209
+ * @param stream - Readable stream of model data
210
+ */
211
+ async saveModelFromStream(key, stream) {
212
+ return this.withWriteLock(key, () => this._saveStreamImpl(key, stream));
213
+ }
214
+ async _saveModelImpl(key, data) {
215
+ if (!this.dirHandle || !this._isReady) {
216
+ throw new Error('LocalFileStorage not ready — call chooseDirectory() or restoreDirectory() first.');
217
+ }
218
+ const filename = this.sanitizeFilename(key);
219
+ const fileHandle = await this.dirHandle.getFileHandle(filename, { create: true });
220
+ const writable = await fileHandle.createWritable();
221
+ try {
222
+ await writable.write(data);
223
+ await writable.close();
224
+ logger.info(`Saved model to local storage: ${filename} (${(data.byteLength / 1024 / 1024).toFixed(1)} MB)`);
225
+ }
226
+ catch (err) {
227
+ try {
228
+ await writable.abort();
229
+ }
230
+ catch { /* ignore */ }
231
+ throw err;
232
+ }
233
+ }
234
+ async _saveStreamImpl(key, stream) {
235
+ if (!this.dirHandle || !this._isReady) {
236
+ throw new Error('LocalFileStorage not ready — call chooseDirectory() or restoreDirectory() first.');
237
+ }
238
+ const filename = this.sanitizeFilename(key);
239
+ const fileHandle = await this.dirHandle.getFileHandle(filename, { create: true });
240
+ const writable = await fileHandle.createWritable();
241
+ try {
242
+ const reader = stream.getReader();
243
+ while (true) {
244
+ const { done, value } = await reader.read();
245
+ if (done)
246
+ break;
247
+ await writable.write(value);
248
+ }
249
+ await writable.close();
250
+ logger.info(`Streamed model to local storage: ${filename}`);
251
+ }
252
+ catch (err) {
253
+ try {
254
+ await writable.abort();
255
+ }
256
+ catch { /* ignore */ }
257
+ throw err;
258
+ }
259
+ }
260
+ /**
261
+ * Per-key write lock: ensures only one write operation per key at a time.
262
+ * Concurrent calls for the same key will be serialized.
263
+ */
264
+ async withWriteLock(key, fn) {
265
+ const prev = this.writeLocks.get(key) ?? Promise.resolve();
266
+ const next = prev.then(fn, fn); // chain even if previous rejected
267
+ this.writeLocks.set(key, next);
268
+ try {
269
+ await next;
270
+ }
271
+ finally {
272
+ // Clean up lock if this was the last write in the chain
273
+ if (this.writeLocks.get(key) === next) {
274
+ this.writeLocks.delete(key);
275
+ }
276
+ }
277
+ }
278
+ /**
279
+ * Load model data from the local filesystem.
280
+ * @param key - Model identifier
281
+ * @returns Model data, or null if not found
282
+ */
283
+ async loadModel(key) {
284
+ if (!this.dirHandle || !this._isReady)
285
+ return null;
286
+ try {
287
+ const filename = this.sanitizeFilename(key);
288
+ const fileHandle = await this.dirHandle.getFileHandle(filename);
289
+ const file = await fileHandle.getFile();
290
+ logger.info(`Loaded model from local storage: ${filename} (${(file.size / 1024 / 1024).toFixed(1)} MB)`);
291
+ return await file.arrayBuffer();
292
+ }
293
+ catch {
294
+ return null; // File not found
295
+ }
296
+ }
297
+ /**
298
+ * Check if a model file exists in local storage.
299
+ * @param key - Model identifier
300
+ */
301
+ async hasModel(key) {
302
+ if (!this.dirHandle || !this._isReady)
303
+ return false;
304
+ try {
305
+ const filename = this.sanitizeFilename(key);
306
+ await this.dirHandle.getFileHandle(filename);
307
+ return true;
308
+ }
309
+ catch {
310
+ return false;
311
+ }
312
+ }
313
+ /**
314
+ * Delete a model file from local storage.
315
+ * @param key - Model identifier
316
+ */
317
+ async deleteModel(key) {
318
+ if (!this.dirHandle || !this._isReady)
319
+ return;
320
+ try {
321
+ const filename = this.sanitizeFilename(key);
322
+ await this.dirHandle.removeEntry(filename);
323
+ logger.info(`Deleted model from local storage: ${filename}`);
324
+ }
325
+ catch {
326
+ // File doesn't exist
327
+ }
328
+ }
329
+ /**
330
+ * Get file size without reading into memory.
331
+ * @param key - Model identifier
332
+ */
333
+ async getFileSize(key) {
334
+ if (!this.dirHandle || !this._isReady)
335
+ return null;
336
+ try {
337
+ const filename = this.sanitizeFilename(key);
338
+ const fileHandle = await this.dirHandle.getFileHandle(filename);
339
+ const file = await fileHandle.getFile();
340
+ return file.size;
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ }
346
+ /**
347
+ * List all model files in the directory.
348
+ */
349
+ async listModels() {
350
+ if (!this.dirHandle || !this._isReady)
351
+ return [];
352
+ const models = [];
353
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
354
+ for await (const [name, handle] of this.dirHandle.entries()) {
355
+ if (handle.kind === 'file') {
356
+ const file = await handle.getFile();
357
+ models.push({
358
+ id: name,
359
+ sizeBytes: file.size,
360
+ lastModified: file.lastModified,
361
+ });
362
+ }
363
+ }
364
+ return models;
365
+ }
366
+ // -------------------------------------------------------------------------
367
+ // IndexedDB Handle Persistence
368
+ // -------------------------------------------------------------------------
369
+ async openDB() {
370
+ return new Promise((resolve, reject) => {
371
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
372
+ request.onupgradeneeded = () => {
373
+ const db = request.result;
374
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
375
+ db.createObjectStore(STORE_NAME);
376
+ }
377
+ };
378
+ request.onsuccess = () => resolve(request.result);
379
+ request.onerror = () => reject(request.error);
380
+ });
381
+ }
382
+ async storeHandle(handle) {
383
+ try {
384
+ const db = await this.openDB();
385
+ const tx = db.transaction(STORE_NAME, 'readwrite');
386
+ tx.objectStore(STORE_NAME).put(handle, HANDLE_KEY);
387
+ await new Promise((resolve, reject) => {
388
+ tx.oncomplete = () => resolve();
389
+ tx.onerror = () => reject(tx.error);
390
+ });
391
+ db.close();
392
+ logger.debug('Directory handle stored in IndexedDB');
393
+ }
394
+ catch (err) {
395
+ const message = err instanceof Error ? err.message : String(err);
396
+ logger.warning(`Failed to store handle in IndexedDB: ${message}`);
397
+ }
398
+ }
399
+ async retrieveHandle() {
400
+ try {
401
+ const db = await this.openDB();
402
+ const tx = db.transaction(STORE_NAME, 'readonly');
403
+ const handle = await new Promise((resolve, reject) => {
404
+ const req = tx.objectStore(STORE_NAME).get(HANDLE_KEY);
405
+ req.onsuccess = () => resolve(req.result ?? null);
406
+ req.onerror = () => reject(req.error);
407
+ });
408
+ db.close();
409
+ return handle;
410
+ }
411
+ catch (err) {
412
+ const message = err instanceof Error ? err.message : String(err);
413
+ logger.warning(`Failed to retrieve handle from IndexedDB: ${message}`);
414
+ return null;
415
+ }
416
+ }
417
+ // -------------------------------------------------------------------------
418
+ // Helpers
419
+ // -------------------------------------------------------------------------
420
+ /**
421
+ * Sanitize a key for use as a filename.
422
+ * Keeps alphanumeric, dots, dashes, underscores. Replaces everything else.
423
+ */
424
+ sanitizeFilename(key) {
425
+ return key.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
426
+ }
427
+ }
428
+ //# sourceMappingURL=LocalFileStorage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LocalFileStorage.js","sourceRoot":"","sources":["../../src/Infrastructure/LocalFileStorage.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { OPFSStorage } from './OPFSStorage';
9
9
  import type { MetadataMap } from './OPFSStorage';
10
+ import type { LocalFileStorage } from './LocalFileStorage';
10
11
  import type { ManagedModel } from './ModelRegistry';
11
12
  import type { ModelRegistry } from './ModelRegistry';
12
13
  /** Candidate model that could be evicted to free space. */
@@ -33,6 +34,11 @@ export interface QuotaCheckResult {
33
34
  export declare class ModelDownloader {
34
35
  private readonly storage;
35
36
  private readonly registry;
37
+ /**
38
+ * Optional local filesystem storage. When configured, models are saved
39
+ * to the user's chosen directory instead of OPFS. Set via setLocalFileStorage().
40
+ */
41
+ private localFileStorage;
36
42
  /**
37
43
  * In-memory fallback cache for models that were downloaded successfully
38
44
  * but failed to persist to OPFS (e.g. storage quota exceeded).
@@ -40,6 +46,11 @@ export declare class ModelDownloader {
40
46
  */
41
47
  private readonly memoryCache;
42
48
  constructor(registry: ModelRegistry, storage: OPFSStorage);
49
+ /**
50
+ * Set the local file storage backend.
51
+ * When configured and ready, models are saved/loaded from the local filesystem first.
52
+ */
53
+ setLocalFileStorage(storage: LocalFileStorage): void;
43
54
  /**
44
55
  * Check whether a model will fit in OPFS without eviction.
45
56
  *
@@ -64,7 +75,7 @@ export declare class ModelDownloader {
64
75
  * URLs are validated before fetching to prevent SSRF and enforce HTTPS.
65
76
  */
66
77
  downloadFile(url: string, onProgress?: (progress: number, bytesDownloaded: number, totalBytes: number) => void): Promise<Uint8Array>;
67
- /** Store data in OPFS via OPFSStorage. Auto-evicts old models if quota is exceeded, falls back to in-memory cache. */
78
+ /** Store data, preferring local filesystem when available, then OPFS, then memory cache. */
68
79
  storeInOPFS(key: string, data: Uint8Array): Promise<void>;
69
80
  /**
70
81
  * Evict old models from OPFS to free space for a new model.
@@ -76,15 +87,20 @@ export declare class ModelDownloader {
76
87
  * the other, separated by "__" (e.g. "modelA" and "modelA__mmproj-...").
77
88
  */
78
89
  private evictOPFSModels;
79
- /** Load data from OPFS via OPFSStorage, falling back to in-memory cache. */
90
+ /**
91
+ * Store a file from a ReadableStream, avoiding loading the entire file into memory.
92
+ * Priority: local filesystem > OPFS. Falls back to buffered write if streaming not supported.
93
+ */
94
+ storeStreamInOPFS(key: string, stream: ReadableStream<Uint8Array>): Promise<void>;
95
+ /** Load data from storage. Priority: local filesystem > OPFS > memory cache. */
80
96
  loadFromOPFS(key: string): Promise<Uint8Array | null>;
81
- /** Check existence in OPFS or in-memory cache. */
97
+ /** Check existence in local storage, OPFS, or in-memory cache. */
82
98
  existsInOPFS(key: string): Promise<boolean>;
83
99
  /** Check if data exists in actual OPFS storage (NOT memory cache). */
84
100
  existsInActualOPFS(key: string): Promise<boolean>;
85
- /** Delete from OPFS and memory cache. */
101
+ /** Delete from all storage backends (local filesystem, OPFS, memory cache). */
86
102
  deleteFromOPFS(key: string): Promise<void>;
87
- /** Get file size from OPFS without reading into memory. */
103
+ /** Get file size from storage without reading into memory. */
88
104
  getOPFSFileSize(key: string): Promise<number | null>;
89
105
  /**
90
106
  * Build an OPFS key for additional files (e.g., mmproj).
@@ -1 +1 @@
1
- {"version":3,"file":"ModelDownloader.d.ts","sourceRoot":"","sources":["../../src/Infrastructure/ModelDownloader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iBAAiB,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMrD,2DAA2D;AAC3D,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAC;IACd,4CAA4C;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;CAC7C;AAkED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IAEzC;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiC;gBAEjD,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW;IASzD;;;;;;;;;;OAUG;IACG,iBAAiB,CACrB,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,WAAW,EACrB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,gBAAgB,CAAC;IAmD5B;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyGnD;;;;;OAKG;IACG,YAAY,CAChB,GAAG,EAAE,MAAM,EACX,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,GACnF,OAAO,CAAC,UAAU,CAAC;IA+BtB,sHAAsH;IAChH,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAuC/D;;;;;;;;OAQG;YACW,eAAe;IAmD7B,4EAA4E;IACtE,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IA6B3D,kDAAkD;IAC5C,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAKjD,sEAAsE;IAChE,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIvD,yCAAyC;IACnC,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKhD,2DAA2D;IACrD,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQ1D;;;;OAIG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI5D,6DAA6D;IAC7D,OAAO,CAAC,oBAAoB;CAS7B"}
1
+ {"version":3,"file":"ModelDownloader.d.ts","sourceRoot":"","sources":["../../src/Infrastructure/ModelDownloader.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iBAAiB,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAMrD,2DAA2D;AAC3D,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,OAAO,CAAC;IACd,4CAA4C;IAC5C,cAAc,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;CAC7C;AAkED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IAEzC;;;OAGG;IACH,OAAO,CAAC,gBAAgB,CAAiC;IAEzD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiC;gBAEjD,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW;IAKzD;;;OAGG;IACH,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAQpD;;;;;;;;;;OAUG;IACG,iBAAiB,CACrB,KAAK,EAAE,YAAY,EACnB,QAAQ,EAAE,WAAW,EACrB,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC,gBAAgB,CAAC;IAmD5B;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAyGnD;;;;;OAKG;IACG,YAAY,CAChB,GAAG,EAAE,MAAM,EACX,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,GACnF,OAAO,CAAC,UAAU,CAAC;IA+BtB,4FAA4F;IACtF,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAoD/D;;;;;;;;OAQG;YACW,eAAe;IAmD7B;;;OAGG;IACG,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBvF,gFAAgF;IAC1E,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAuC3D,kEAAkE;IAC5D,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASjD,sEAAsE;IAChE,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIvD,+EAA+E;IACzE,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhD,8DAA8D;IACxD,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAY1D;;;;OAIG;IACH,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI5D,6DAA6D;IAC7D,OAAO,CAAC,oBAAoB;CAS7B"}
@@ -65,6 +65,11 @@ const logger = new SDKLogger('ModelDownloader');
65
65
  export class ModelDownloader {
66
66
  storage;
67
67
  registry;
68
+ /**
69
+ * Optional local filesystem storage. When configured, models are saved
70
+ * to the user's chosen directory instead of OPFS. Set via setLocalFileStorage().
71
+ */
72
+ localFileStorage = null;
68
73
  /**
69
74
  * In-memory fallback cache for models that were downloaded successfully
70
75
  * but failed to persist to OPFS (e.g. storage quota exceeded).
@@ -75,6 +80,13 @@ export class ModelDownloader {
75
80
  this.registry = registry;
76
81
  this.storage = storage;
77
82
  }
83
+ /**
84
+ * Set the local file storage backend.
85
+ * When configured and ready, models are saved/loaded from the local filesystem first.
86
+ */
87
+ setLocalFileStorage(storage) {
88
+ this.localFileStorage = storage;
89
+ }
78
90
  // ---------------------------------------------------------------------------
79
91
  // Public API
80
92
  // ---------------------------------------------------------------------------
@@ -270,11 +282,24 @@ export class ModelDownloader {
270
282
  }
271
283
  return data;
272
284
  }
273
- /** Store data in OPFS via OPFSStorage. Auto-evicts old models if quota is exceeded, falls back to in-memory cache. */
285
+ /** Store data, preferring local filesystem when available, then OPFS, then memory cache. */
274
286
  async storeInOPFS(key, data) {
275
287
  const sizeMB = (data.length / 1024 / 1024).toFixed(1);
288
+ // Try local filesystem first (permanent, no quota issues)
289
+ if (this.localFileStorage?.isReady) {
290
+ try {
291
+ await this.localFileStorage.saveModel(key, data.buffer);
292
+ logger.info(`Stored ${key} in local storage (${sizeMB} MB)`);
293
+ this.memoryCache.delete(key);
294
+ return;
295
+ }
296
+ catch (err) {
297
+ const msg = err instanceof Error ? err.message : String(err);
298
+ logger.warning(`Local storage write failed for "${key}": ${msg}, falling back to OPFS`);
299
+ }
300
+ }
276
301
  try {
277
- // First attempt
302
+ // First attempt — OPFS
278
303
  await this.storage.saveModel(key, data.buffer);
279
304
  logger.info(`Stored ${key} in OPFS (${sizeMB} MB)`);
280
305
  this.memoryCache.delete(key);
@@ -361,9 +386,43 @@ export class ModelDownloader {
361
386
  }
362
387
  }
363
388
  }
364
- /** Load data from OPFS via OPFSStorage, falling back to in-memory cache. */
389
+ /**
390
+ * Store a file from a ReadableStream, avoiding loading the entire file into memory.
391
+ * Priority: local filesystem > OPFS. Falls back to buffered write if streaming not supported.
392
+ */
393
+ async storeStreamInOPFS(key, stream) {
394
+ // Try local filesystem first (permanent, no quota issues)
395
+ if (this.localFileStorage?.isReady) {
396
+ try {
397
+ await this.localFileStorage.saveModelFromStream(key, stream);
398
+ return;
399
+ }
400
+ catch (err) {
401
+ const msg = err instanceof Error ? err.message : String(err);
402
+ logger.warning(`Local storage stream write failed for "${key}": ${msg}, falling back to OPFS`);
403
+ }
404
+ }
405
+ try {
406
+ await this.storage.saveModelFromStream(key, stream);
407
+ }
408
+ catch (err) {
409
+ const msg = err instanceof Error ? err.message : String(err);
410
+ logger.warning(`OPFS stream store failed for "${key}": ${msg}`);
411
+ throw err;
412
+ }
413
+ }
414
+ /** Load data from storage. Priority: local filesystem > OPFS > memory cache. */
365
415
  async loadFromOPFS(key) {
366
- // Try OPFS first (persistent storage)
416
+ // Try local filesystem first (permanent storage)
417
+ if (this.localFileStorage?.isReady) {
418
+ const localBuffer = await this.localFileStorage.loadModel(key);
419
+ if (localBuffer && localBuffer.byteLength > 0) {
420
+ const sizeMB = localBuffer.byteLength / 1024 / 1024;
421
+ logger.debug(`Loading ${key} from local storage (${sizeMB.toFixed(1)} MB)`);
422
+ return new Uint8Array(localBuffer);
423
+ }
424
+ }
425
+ // Try OPFS (persistent browser storage)
367
426
  const buffer = await this.storage.loadModel(key);
368
427
  if (buffer && buffer.byteLength > 0) {
369
428
  const sizeMB = buffer.byteLength / 1024 / 1024;
@@ -387,8 +446,13 @@ export class ModelDownloader {
387
446
  }
388
447
  return null;
389
448
  }
390
- /** Check existence in OPFS or in-memory cache. */
449
+ /** Check existence in local storage, OPFS, or in-memory cache. */
391
450
  async existsInOPFS(key) {
451
+ if (this.localFileStorage?.isReady) {
452
+ const localExists = await this.localFileStorage.hasModel(key);
453
+ if (localExists)
454
+ return true;
455
+ }
392
456
  if (this.memoryCache.has(key))
393
457
  return true;
394
458
  return this.storage.hasModel(key);
@@ -397,13 +461,21 @@ export class ModelDownloader {
397
461
  async existsInActualOPFS(key) {
398
462
  return this.storage.hasModel(key);
399
463
  }
400
- /** Delete from OPFS and memory cache. */
464
+ /** Delete from all storage backends (local filesystem, OPFS, memory cache). */
401
465
  async deleteFromOPFS(key) {
402
466
  this.memoryCache.delete(key);
467
+ if (this.localFileStorage?.isReady) {
468
+ await this.localFileStorage.deleteModel(key);
469
+ }
403
470
  await this.storage.deleteModel(key);
404
471
  }
405
- /** Get file size from OPFS without reading into memory. */
472
+ /** Get file size from storage without reading into memory. */
406
473
  async getOPFSFileSize(key) {
474
+ if (this.localFileStorage?.isReady) {
475
+ const localSize = await this.localFileStorage.getFileSize(key);
476
+ if (localSize !== null)
477
+ return localSize;
478
+ }
407
479
  return this.storage.getFileSize(key);
408
480
  }
409
481
  // ---------------------------------------------------------------------------