yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
wasm_storage_quota.cc
Go to the documentation of this file.
1// clang-format off
2#ifdef __EMSCRIPTEN__
3
5
6#include <emscripten.h>
7#include <emscripten/bind.h>
8#include <algorithm>
9#include <chrono>
10#include <cstring>
11#include <iostream>
12#include <queue>
13
14namespace yaze {
15namespace app {
16namespace platform {
17
18namespace {
19
20// Callback management for async operations
21struct CallbackManager {
22 static CallbackManager& Get() {
23 static CallbackManager instance;
24 return instance;
25 }
26
27 int RegisterStorageCallback(
28 std::function<void(const WasmStorageQuota::StorageInfo&)> cb) {
29 std::lock_guard<std::mutex> lock(mutex_);
30 int id = next_id_++;
31 storage_callbacks_[id] = cb;
32 return id;
33 }
34
35 int RegisterCompressCallback(
36 std::function<void(std::vector<uint8_t>)> cb) {
37 std::lock_guard<std::mutex> lock(mutex_);
38 int id = next_id_++;
39 compress_callbacks_[id] = cb;
40 return id;
41 }
42
43 void InvokeStorageCallback(int id, size_t used, size_t quota, bool persistent) {
44 std::lock_guard<std::mutex> lock(mutex_);
45 auto it = storage_callbacks_.find(id);
46 if (it != storage_callbacks_.end()) {
47 WasmStorageQuota::StorageInfo info;
48 info.used_bytes = used;
49 info.quota_bytes = quota;
50 info.usage_percent = quota > 0 ? (float(used) / float(quota) * 100.0f) : 0.0f;
51 info.persistent = persistent;
52 it->second(info);
53 storage_callbacks_.erase(it);
54 }
55 }
56
57 void InvokeCompressCallback(int id, uint8_t* data, size_t size) {
58 std::lock_guard<std::mutex> lock(mutex_);
59 auto it = compress_callbacks_.find(id);
60 if (it != compress_callbacks_.end()) {
61 std::vector<uint8_t> result;
62 if (data && size > 0) {
63 result.assign(data, data + size);
64 free(data); // Free the allocated memory from JS
65 }
66 it->second(result);
67 compress_callbacks_.erase(it);
68 }
69 }
70
71 private:
72 std::mutex mutex_;
73 int next_id_ = 1;
74 std::map<int, std::function<void(const WasmStorageQuota::StorageInfo&)>> storage_callbacks_;
75 std::map<int, std::function<void(std::vector<uint8_t>)>> compress_callbacks_;
76};
77
78} // namespace
79
80// External C functions called from JavaScript
81extern "C" {
82
83EMSCRIPTEN_KEEPALIVE
84void wasm_storage_quota_estimate_callback(int callback_id, double used,
85 double quota, int persistent) {
86 CallbackManager::Get().InvokeStorageCallback(
87 callback_id, size_t(used), size_t(quota), persistent != 0);
88}
89
90EMSCRIPTEN_KEEPALIVE
91void wasm_compress_callback(int callback_id, uint8_t* data, size_t size) {
92 CallbackManager::Get().InvokeCompressCallback(callback_id, data, size);
93}
94
95EMSCRIPTEN_KEEPALIVE
96void wasm_decompress_callback(int callback_id, uint8_t* data, size_t size) {
97 CallbackManager::Get().InvokeCompressCallback(callback_id, data, size);
98}
99
100} // extern "C"
101
102// External JS functions declared in header
103extern void wasm_storage_quota_estimate(int callback_id);
104extern void wasm_compress_data(const uint8_t* data, size_t size, int callback_id);
105extern void wasm_decompress_data(const uint8_t* data, size_t size, int callback_id);
106extern double wasm_get_timestamp_ms();
107extern int wasm_compression_available();
108
109// WasmStorageQuota implementation
110
111WasmStorageQuota& WasmStorageQuota::Get() {
112 static WasmStorageQuota instance;
113 return instance;
114}
115
117 // Check for required APIs
118 return EM_ASM_INT({
119 return (navigator.storage &&
120 navigator.storage.estimate &&
121 indexedDB) ? 1 : 0;
122 }) != 0;
123}
124
126 std::function<void(const StorageInfo&)> callback) {
127 if (!callback) return;
128
129 // Check if we recently checked (within 5 seconds)
130 double now = wasm_get_timestamp_ms();
131 if (now - last_quota_check_time_.load() < 5000.0 &&
132 last_storage_info_.quota_bytes > 0) {
133 callback(last_storage_info_);
134 return;
135 }
136
137 int callback_id = CallbackManager::Get().RegisterStorageCallback(
138 [this, callback](const StorageInfo& info) {
139 last_storage_info_ = info;
140 last_quota_check_time_.store(wasm_get_timestamp_ms());
141 callback(info);
142 });
143
144 wasm_storage_quota_estimate(callback_id);
145}
146
147void WasmStorageQuota::CompressData(
148 const std::vector<uint8_t>& input,
149 std::function<void(std::vector<uint8_t>)> callback) {
150 if (!callback || input.empty()) {
151 if (callback) callback(std::vector<uint8_t>());
152 return;
153 }
154
155 int callback_id = CallbackManager::Get().RegisterCompressCallback(callback);
156 wasm_compress_data(input.data(), input.size(), callback_id);
157}
158
159void WasmStorageQuota::DecompressData(
160 const std::vector<uint8_t>& input,
161 std::function<void(std::vector<uint8_t>)> callback) {
162 if (!callback || input.empty()) {
163 if (callback) callback(std::vector<uint8_t>());
164 return;
165 }
166
167 int callback_id = CallbackManager::Get().RegisterCompressCallback(callback);
168 wasm_decompress_data(input.data(), input.size(), callback_id);
169}
170
172 const std::string& key,
173 const std::vector<uint8_t>& data,
174 std::function<void(bool success)> callback) {
175 if (key.empty() || data.empty()) {
176 if (callback) callback(false);
177 return;
178 }
179
180 size_t original_size = data.size();
181
182 // First compress the data
183 CompressData(data, [this, key, original_size, callback](
184 const std::vector<uint8_t>& compressed) {
185 if (compressed.empty()) {
186 if (callback) callback(false);
187 return;
188 }
189
190 // Check quota before storing
191 CheckQuotaAndEvict(compressed.size(), [this, key, compressed, original_size, callback](
192 bool quota_ok) {
193 if (!quota_ok) {
194 if (callback) callback(false);
195 return;
196 }
197
198 // Store the compressed data
199 StoreCompressedData(key, compressed, original_size, callback);
200 });
201 });
202}
203
204void WasmStorageQuota::LoadAndDecompress(
205 const std::string& key,
206 std::function<void(std::vector<uint8_t>)> callback) {
207 if (key.empty()) {
208 if (callback) callback(std::vector<uint8_t>());
209 return;
210 }
211
212 // Load compressed data from storage
213 LoadCompressedData(key, [this, key, callback](
214 const std::vector<uint8_t>& compressed,
215 size_t original_size) {
216 if (compressed.empty()) {
217 if (callback) callback(std::vector<uint8_t>());
218 return;
219 }
220
221 // Update access time
222 UpdateAccessTime(key);
223
224 // Decompress the data
225 DecompressData(compressed, callback);
226 });
227}
228
229void WasmStorageQuota::StoreCompressedData(
230 const std::string& key,
231 const std::vector<uint8_t>& compressed_data,
232 size_t original_size,
233 std::function<void(bool)> callback) {
234
235 // Use the existing WasmStorage for actual storage
236 EM_ASM({
237 var key = UTF8ToString($0);
238 var dataPtr = $1;
239 var dataSize = $2;
240 var originalSize = $3;
241 var callbackPtr = $4;
242
243 if (!Module._yazeDB) {
244 console.error('[StorageQuota] Database not initialized');
245 Module.dynCall_vi(callbackPtr, 0);
246 return;
247 }
248
249 var data = new Uint8Array(Module.HEAPU8.buffer, dataPtr, dataSize);
250 var metadata = {
251 compressed_size: dataSize,
252 original_size: originalSize,
253 last_access: Date.now(),
254 compression_ratio: originalSize > 0 ? (dataSize / originalSize) : 1.0
255 };
256
257 var transaction = Module._yazeDB.transaction(['roms', 'metadata'], 'readwrite');
258 var romStore = transaction.objectStore('roms');
259 var metaStore = transaction.objectStore('metadata');
260
261 // Store compressed data
262 var dataRequest = romStore.put(data, key);
263 // Store metadata
264 var metaRequest = metaStore.put(metadata, key);
265
266 transaction.oncomplete = function() {
267 Module.dynCall_vi(callbackPtr, 1);
268 };
269
270 transaction.onerror = function() {
271 console.error('[StorageQuota] Failed to store compressed data');
272 Module.dynCall_vi(callbackPtr, 0);
273 };
274 }, key.c_str(), compressed_data.data(), compressed_data.size(),
275 original_size, callback ? new std::function<void(bool)>(callback) : nullptr);
276
277 // Update local metadata cache
278 UpdateMetadata(key, compressed_data.size(), original_size);
279}
280
281void WasmStorageQuota::LoadCompressedData(
282 const std::string& key,
283 std::function<void(std::vector<uint8_t>, size_t)> callback) {
284
285 EM_ASM({
286 var key = UTF8ToString($0);
287 var callbackPtr = $1;
288
289 if (!Module._yazeDB) {
290 console.error('[StorageQuota] Database not initialized');
291 Module.dynCall_viii(callbackPtr, 0, 0, 0);
292 return;
293 }
294
295 var transaction = Module._yazeDB.transaction(['roms', 'metadata'], 'readonly');
296 var romStore = transaction.objectStore('roms');
297 var metaStore = transaction.objectStore('metadata');
298
299 var dataRequest = romStore.get(key);
300 var metaRequest = metaStore.get(key);
301
302 var romData = null;
303 var metadata = null;
304
305 dataRequest.onsuccess = function() {
306 romData = dataRequest.result;
307 checkComplete();
308 };
309
310 metaRequest.onsuccess = function() {
311 metadata = metaRequest.result;
312 checkComplete();
313 };
314
315 function checkComplete() {
316 if (romData !== null && metadata !== null) {
317 if (romData && metadata) {
318 var ptr = Module._malloc(romData.length);
319 Module.HEAPU8.set(romData, ptr);
320 Module.dynCall_viii(callbackPtr, ptr, romData.length,
321 metadata.original_size || romData.length);
322 } else {
323 Module.dynCall_viii(callbackPtr, 0, 0, 0);
324 }
325 }
326 }
327
328 transaction.onerror = function() {
329 console.error('[StorageQuota] Failed to load compressed data');
330 Module.dynCall_viii(callbackPtr, 0, 0, 0);
331 };
332 }, key.c_str(), callback ? new std::function<void(std::vector<uint8_t>, size_t)>(
333 callback) : nullptr);
334}
335
336void WasmStorageQuota::UpdateAccessTime(const std::string& key) {
337 double now = wasm_get_timestamp_ms();
338
339 {
340 std::lock_guard<std::mutex> lock(mutex_);
341 access_times_[key] = now;
342 if (item_metadata_.count(key)) {
343 item_metadata_[key].last_access_time = now;
344 }
345 }
346
347 // Update in IndexedDB
348 EM_ASM({
349 var key = UTF8ToString($0);
350 var timestamp = $1;
351
352 if (!Module._yazeDB) return;
353
354 var transaction = Module._yazeDB.transaction(['metadata'], 'readwrite');
355 var store = transaction.objectStore('metadata');
356
357 var request = store.get(key);
358 request.onsuccess = function() {
359 var metadata = request.result || {};
360 metadata.last_access = timestamp;
361 store.put(metadata, key);
362 };
363 }, key.c_str(), now);
364}
365
366void WasmStorageQuota::UpdateMetadata(const std::string& key,
367 size_t compressed_size,
368 size_t original_size) {
369 std::lock_guard<std::mutex> lock(mutex_);
370
371 StorageItem item;
372 item.key = key;
373 item.compressed_size = compressed_size;
374 item.original_size = original_size;
375 item.last_access_time = wasm_get_timestamp_ms();
376 item.compression_ratio = original_size > 0 ?
377 float(compressed_size) / float(original_size) : 1.0f;
378
379 item_metadata_[key] = item;
380 access_times_[key] = item.last_access_time;
381}
382
383void WasmStorageQuota::GetStoredItems(
384 std::function<void(std::vector<StorageItem>)> callback) {
385 if (!callback) return;
386
387 LoadMetadata([this, callback]() {
388 std::lock_guard<std::mutex> lock(mutex_);
389 std::vector<StorageItem> items;
390 items.reserve(item_metadata_.size());
391
392 for (const auto& [key, item] : item_metadata_) {
393 items.push_back(item);
394 }
395
396 // Sort by last access time (most recent first)
397 std::sort(items.begin(), items.end(),
398 [](const StorageItem& a, const StorageItem& b) {
399 return a.last_access_time > b.last_access_time;
400 });
401
402 callback(items);
403 });
404}
405
406void WasmStorageQuota::LoadMetadata(std::function<void()> callback) {
407 if (metadata_loaded_.load()) {
408 if (callback) callback();
409 return;
410 }
411
412 EM_ASM({
413 var callbackPtr = $0;
414
415 if (!Module._yazeDB) {
416 if (callbackPtr) Module.dynCall_v(callbackPtr);
417 return;
418 }
419
420 var transaction = Module._yazeDB.transaction(['metadata'], 'readonly');
421 var store = transaction.objectStore('metadata');
422 var request = store.getAllKeys();
423
424 request.onsuccess = function() {
425 var keys = request.result;
426 var metadata = {};
427 var pending = keys.length;
428
429 if (pending === 0) {
430 if (callbackPtr) Module.dynCall_v(callbackPtr);
431 return;
432 }
433
434 keys.forEach(function(key) {
435 var getRequest = store.get(key);
436 getRequest.onsuccess = function() {
437 metadata[key] = getRequest.result;
438 pending--;
439 if (pending === 0) {
440 // Pass metadata back to C++
441 Module.storageQuotaMetadata = metadata;
442 if (callbackPtr) Module.dynCall_v(callbackPtr);
443 }
444 };
445 });
446 };
447 }, callback ? new std::function<void()>(callback) : nullptr);
448
449 // After JS callback, process the metadata
450 std::lock_guard<std::mutex> lock(mutex_);
451
452 // Access JS metadata object and populate C++ structures
453 emscripten::val metadata = emscripten::val::global("Module")["storageQuotaMetadata"];
454 if (metadata.as<bool>()) {
455 // Process each key in the metadata
456 // Note: This is simplified - in production you'd iterate the JS object properly
457 metadata_loaded_.store(true);
458 }
459}
460
461void WasmStorageQuota::EvictOldest(int count,
462 std::function<void(int evicted)> callback) {
463 if (count <= 0) {
464 if (callback) callback(0);
465 return;
466 }
467
468 GetStoredItems([this, count, callback](const std::vector<StorageItem>& items) {
469 // Items are already sorted by access time (newest first)
470 // We want to evict from the end (oldest)
471 int to_evict = std::min(count, static_cast<int>(items.size()));
472 int evicted = 0;
473
474 for (int i = items.size() - to_evict; i < items.size(); ++i) {
475 DeleteItem(items[i].key, [&evicted](bool success) {
476 if (success) evicted++;
477 });
478 }
479
480 if (callback) callback(evicted);
481 });
482}
483
484void WasmStorageQuota::EvictToTarget(float target_percent,
485 std::function<void(int evicted)> callback) {
486 if (target_percent <= 0 || target_percent >= 100) {
487 if (callback) callback(0);
488 return;
489 }
490
491 GetStorageInfo([this, target_percent, callback](const StorageInfo& info) {
492 if (info.usage_percent <= target_percent) {
493 if (callback) callback(0);
494 return;
495 }
496
497 // Calculate how much space we need to free
498 size_t target_bytes = size_t(info.quota_bytes * target_percent / 100.0f);
499 size_t bytes_to_free = info.used_bytes - target_bytes;
500
501 GetStoredItems([this, bytes_to_free, callback](
502 const std::vector<StorageItem>& items) {
503 size_t freed = 0;
504 int evicted = 0;
505
506 // Evict oldest items until we've freed enough space
507 for (auto it = items.rbegin(); it != items.rend(); ++it) {
508 if (freed >= bytes_to_free) break;
509
510 DeleteItem(it->key, [&evicted, &freed, it](bool success) {
511 if (success) {
512 evicted++;
513 freed += it->compressed_size;
514 }
515 });
516 }
517
518 if (callback) callback(evicted);
519 });
520 });
521}
522
523void WasmStorageQuota::CheckQuotaAndEvict(size_t new_size_bytes,
524 std::function<void(bool)> callback) {
525 GetStorageInfo([this, new_size_bytes, callback](const StorageInfo& info) {
526 // Check if we have enough space
527 size_t projected_usage = info.used_bytes + new_size_bytes;
528 float projected_percent = info.quota_bytes > 0 ?
529 (float(projected_usage) / float(info.quota_bytes) * 100.0f) : 100.0f;
530
531 if (projected_percent <= kWarningThreshold) {
532 // Plenty of space available
533 if (callback) callback(true);
534 return;
535 }
536
537 if (projected_percent > kCriticalThreshold) {
538 // Need to evict to make space
539 std::cerr << "[StorageQuota] Approaching quota limit, evicting old ROMs..."
540 << std::endl;
541
542 EvictToTarget(kDefaultTargetUsage, [callback](int evicted) {
543 std::cerr << "[StorageQuota] Evicted " << evicted << " items"
544 << std::endl;
545 if (callback) callback(evicted > 0);
546 });
547 } else {
548 // Warning zone but still ok
549 std::cerr << "[StorageQuota] Warning: Storage at " << projected_percent
550 << "% after this operation" << std::endl;
551 if (callback) callback(true);
552 }
553 });
554}
555
556void WasmStorageQuota::DeleteItem(const std::string& key,
557 std::function<void(bool success)> callback) {
558 EM_ASM({
559 var key = UTF8ToString($0);
560 var callbackPtr = $1;
561
562 if (!Module._yazeDB) {
563 if (callbackPtr) Module.dynCall_vi(callbackPtr, 0);
564 return;
565 }
566
567 var transaction = Module._yazeDB.transaction(['roms', 'metadata'], 'readwrite');
568 var romStore = transaction.objectStore('roms');
569 var metaStore = transaction.objectStore('metadata');
570
571 romStore.delete(key);
572 metaStore.delete(key);
573
574 transaction.oncomplete = function() {
575 if (callbackPtr) Module.dynCall_vi(callbackPtr, 1);
576 };
577
578 transaction.onerror = function() {
579 if (callbackPtr) Module.dynCall_vi(callbackPtr, 0);
580 };
581 }, key.c_str(), callback ? new std::function<void(bool)>(callback) : nullptr);
582
583 // Update local cache
584 {
585 std::lock_guard<std::mutex> lock(mutex_);
586 access_times_.erase(key);
587 item_metadata_.erase(key);
588 }
589}
590
591void WasmStorageQuota::ClearAll(std::function<void()> callback) {
592 EM_ASM({
593 var callbackPtr = $0;
594
595 if (!Module._yazeDB) {
596 if (callbackPtr) Module.dynCall_v(callbackPtr);
597 return;
598 }
599
600 var transaction = Module._yazeDB.transaction(['roms', 'metadata'], 'readwrite');
601 var romStore = transaction.objectStore('roms');
602 var metaStore = transaction.objectStore('metadata');
603
604 romStore.clear();
605 metaStore.clear();
606
607 transaction.oncomplete = function() {
608 if (callbackPtr) Module.dynCall_v(callbackPtr);
609 };
610 }, callback ? new std::function<void()>(callback) : nullptr);
611
612 // Clear local cache
613 {
614 std::lock_guard<std::mutex> lock(mutex_);
615 access_times_.clear();
616 item_metadata_.clear();
617 }
618}
619
620} // namespace platform
621} // namespace app
622} // namespace yaze
623
624#endif // __EMSCRIPTEN__
625
626// clang-format on
void CompressAndStore(const std::string &key, const std::vector< uint8_t > &data, std::function< void(bool success)> callback)
void GetStorageInfo(std::function< void(const StorageInfo &)> callback)
absl::Status LoadMetadata(const Rom &rom, GameData &data)
Definition game_data.cc:136