yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
wasm_storage.cc
Go to the documentation of this file.
1// clang-format off
2#ifdef __EMSCRIPTEN__
3
5
6#include <emscripten.h>
7#include <emscripten/val.h>
8#include <condition_variable>
9#include <cstring>
10#include <mutex>
11
12#include "absl/strings/str_format.h"
13
14namespace yaze {
15namespace platform {
16
17// Static member initialization
18std::atomic<bool> WasmStorage::initialized_{false};
19
20// JavaScript IndexedDB interface using EM_JS
21// All functions use yazeAsyncQueue to serialize async operations
22EM_JS(int, idb_open_database, (const char* db_name, int version), {
23 return Asyncify.handleAsync(function() {
24 var dbName = UTF8ToString(db_name); // Must convert before queueing!
25 var operation = function() {
26 return new Promise(function(resolve, reject) {
27 var request = indexedDB.open(dbName, version);
28 request.onerror = function() {
29 console.error('Failed to open IndexedDB:', request.error);
30 resolve(-1);
31 };
32 request.onsuccess = function() {
33 var db = request.result;
34 Module._yazeDB = db;
35 resolve(0);
36 };
37 request.onupgradeneeded = function(event) {
38 var db = event.target.result;
39 if (!db.objectStoreNames.contains('roms')) {
40 db.createObjectStore('roms');
41 }
42 if (!db.objectStoreNames.contains('projects')) {
43 db.createObjectStore('projects');
44 }
45 if (!db.objectStoreNames.contains('preferences')) {
46 db.createObjectStore('preferences');
47 }
48 };
49 });
50 };
51 // Use async queue if available to prevent concurrent Asyncify operations
52 if (window.yazeAsyncQueue) {
53 return window.yazeAsyncQueue.enqueue(operation);
54 }
55 return operation();
56 });
57});
58
59EM_JS(int, idb_save_binary, (const char* store_name, const char* key, const uint8_t* data, size_t size), {
60 return Asyncify.handleAsync(function() {
61 var storeName = UTF8ToString(store_name);
62 var keyStr = UTF8ToString(key);
63 var dataArray = new Uint8Array(HEAPU8.subarray(data, data + size));
64 var operation = function() {
65 return new Promise(function(resolve, reject) {
66 if (!Module._yazeDB) {
67 console.error('Database not initialized');
68 resolve(-1);
69 return;
70 }
71 var transaction = Module._yazeDB.transaction([storeName], 'readwrite');
72 var store = transaction.objectStore(storeName);
73 var request = store.put(dataArray, keyStr);
74 request.onsuccess = function() { resolve(0); };
75 request.onerror = function() {
76 console.error('Failed to save data:', request.error);
77 resolve(-1);
78 };
79 });
80 };
81 if (window.yazeAsyncQueue) {
82 return window.yazeAsyncQueue.enqueue(operation);
83 }
84 return operation();
85 });
86});
87
88EM_JS(int, idb_load_binary, (const char* store_name, const char* key, uint8_t** out_data, size_t* out_size), {
89 return Asyncify.handleAsync(function() {
90 if (!Module._yazeDB) {
91 console.error('Database not initialized');
92 return -1;
93 }
94 var storeName = UTF8ToString(store_name);
95 var keyStr = UTF8ToString(key);
96 var operation = function() {
97 return new Promise(function(resolve) {
98 var transaction = Module._yazeDB.transaction([storeName], 'readonly');
99 var store = transaction.objectStore(storeName);
100 var request = store.get(keyStr);
101 request.onsuccess = function() {
102 var result = request.result;
103 if (result && result instanceof Uint8Array) {
104 var size = result.length;
105 var ptr = Module._malloc(size);
106 Module.HEAPU8.set(result, ptr);
107 Module.HEAPU32[out_data >> 2] = ptr;
108 Module.HEAPU32[out_size >> 2] = size;
109 resolve(0);
110 } else {
111 resolve(-2);
112 }
113 };
114 request.onerror = function() {
115 console.error('Failed to load data:', request.error);
116 resolve(-1);
117 };
118 });
119 };
120 if (window.yazeAsyncQueue) {
121 return window.yazeAsyncQueue.enqueue(operation);
122 }
123 return operation();
124 });
125});
126
127EM_JS(int, idb_save_string, (const char* store_name, const char* key, const char* value), {
128 return Asyncify.handleAsync(function() {
129 var storeName = UTF8ToString(store_name);
130 var keyStr = UTF8ToString(key);
131 var valueStr = UTF8ToString(value);
132 var operation = function() {
133 return new Promise(function(resolve, reject) {
134 if (!Module._yazeDB) {
135 console.error('Database not initialized');
136 resolve(-1);
137 return;
138 }
139 var transaction = Module._yazeDB.transaction([storeName], 'readwrite');
140 var store = transaction.objectStore(storeName);
141 var request = store.put(valueStr, keyStr);
142 request.onsuccess = function() { resolve(0); };
143 request.onerror = function() {
144 console.error('Failed to save string:', request.error);
145 resolve(-1);
146 };
147 });
148 };
149 if (window.yazeAsyncQueue) {
150 return window.yazeAsyncQueue.enqueue(operation);
151 }
152 return operation();
153 });
154});
155
156EM_JS(char*, idb_load_string, (const char* store_name, const char* key), {
157 return Asyncify.handleAsync(function() {
158 if (!Module._yazeDB) {
159 console.error('Database not initialized');
160 return 0;
161 }
162 var storeName = UTF8ToString(store_name);
163 var keyStr = UTF8ToString(key);
164 var operation = function() {
165 return new Promise(function(resolve) {
166 var transaction = Module._yazeDB.transaction([storeName], 'readonly');
167 var store = transaction.objectStore(storeName);
168 var request = store.get(keyStr);
169 request.onsuccess = function() {
170 var result = request.result;
171 if (result && typeof result === 'string') {
172 var len = lengthBytesUTF8(result) + 1;
173 var ptr = Module._malloc(len);
174 stringToUTF8(result, ptr, len);
175 resolve(ptr);
176 } else {
177 resolve(0);
178 }
179 };
180 request.onerror = function() {
181 console.error('Failed to load string:', request.error);
182 resolve(0);
183 };
184 });
185 };
186 if (window.yazeAsyncQueue) {
187 return window.yazeAsyncQueue.enqueue(operation);
188 }
189 return operation();
190 });
191});
192
193EM_JS(int, idb_delete_entry, (const char* store_name, const char* key), {
194 return Asyncify.handleAsync(function() {
195 var storeName = UTF8ToString(store_name);
196 var keyStr = UTF8ToString(key);
197 var operation = function() {
198 return new Promise(function(resolve, reject) {
199 if (!Module._yazeDB) {
200 console.error('Database not initialized');
201 resolve(-1);
202 return;
203 }
204 var transaction = Module._yazeDB.transaction([storeName], 'readwrite');
205 var store = transaction.objectStore(storeName);
206 var request = store.delete(keyStr);
207 request.onsuccess = function() { resolve(0); };
208 request.onerror = function() {
209 console.error('Failed to delete entry:', request.error);
210 resolve(-1);
211 };
212 });
213 };
214 if (window.yazeAsyncQueue) {
215 return window.yazeAsyncQueue.enqueue(operation);
216 }
217 return operation();
218 });
219});
220
221EM_JS(char*, idb_list_keys, (const char* store_name), {
222 return Asyncify.handleAsync(function() {
223 if (!Module._yazeDB) {
224 console.error('Database not initialized');
225 return 0;
226 }
227 var storeName = UTF8ToString(store_name);
228 var operation = function() {
229 return new Promise(function(resolve) {
230 var transaction = Module._yazeDB.transaction([storeName], 'readonly');
231 var store = transaction.objectStore(storeName);
232 var request = store.getAllKeys();
233 request.onsuccess = function() {
234 var keys = request.result;
235 var jsonStr = JSON.stringify(keys);
236 var len = lengthBytesUTF8(jsonStr) + 1;
237 var ptr = Module._malloc(len);
238 stringToUTF8(jsonStr, ptr, len);
239 resolve(ptr);
240 };
241 request.onerror = function() {
242 console.error('Failed to list keys:', request.error);
243 resolve(0);
244 };
245 });
246 };
247 if (window.yazeAsyncQueue) {
248 return window.yazeAsyncQueue.enqueue(operation);
249 }
250 return operation();
251 });
252});
253
254EM_JS(size_t, idb_get_storage_usage, (), {
255 return Asyncify.handleAsync(function() {
256 if (!Module._yazeDB) {
257 console.error('Database not initialized');
258 return 0;
259 }
260 var operation = function() {
261 return new Promise(function(resolve) {
262 var totalSize = 0;
263 var storeNames = ['roms', 'projects', 'preferences'];
264 var completed = 0;
265
266 storeNames.forEach(function(storeName) {
267 var transaction = Module._yazeDB.transaction([storeName], 'readonly');
268 var store = transaction.objectStore(storeName);
269 var request = store.openCursor();
270
271 request.onsuccess = function(event) {
272 var cursor = event.target.result;
273 if (cursor) {
274 var value = cursor.value;
275 if (value instanceof Uint8Array) {
276 totalSize += value.length;
277 } else if (typeof value === 'string') {
278 totalSize += value.length * 2; // UTF-16 estimation
279 } else if (value) {
280 totalSize += JSON.stringify(value).length * 2;
281 }
282 cursor.continue();
283 } else {
284 completed++;
285 if (completed === storeNames.length) {
286 resolve(totalSize);
287 }
288 }
289 };
290
291 request.onerror = function() {
292 completed++;
293 if (completed === storeNames.length) {
294 resolve(totalSize);
295 }
296 };
297 });
298 });
299 };
300 if (window.yazeAsyncQueue) {
301 return window.yazeAsyncQueue.enqueue(operation);
302 }
303 return operation();
304 });
305});
306
307// Implementation of WasmStorage methods
308absl::Status WasmStorage::Initialize() {
309 // Use compare_exchange for thread-safe initialization
310 bool expected = false;
311 if (!initialized_.compare_exchange_strong(expected, true)) {
312 return absl::OkStatus(); // Already initialized by another thread
313 }
314
315 int result = idb_open_database(kDatabaseName, kDatabaseVersion);
316 if (result != 0) {
317 initialized_.store(false); // Reset on failure
318 return absl::InternalError("Failed to initialize IndexedDB");
319 }
320 return absl::OkStatus();
321}
322
323void WasmStorage::EnsureInitialized() {
324 if (!initialized_.load()) {
325 auto status = Initialize();
326 if (!status.ok()) {
327 emscripten_log(EM_LOG_ERROR, "Failed to initialize WasmStorage: %s", status.ToString().c_str());
328 }
329 }
330}
331
332bool WasmStorage::IsStorageAvailable() {
333 EnsureInitialized();
334 return initialized_.load();
335}
336
337bool WasmStorage::IsWebContext() {
338 return EM_ASM_INT({
339 return (typeof window !== 'undefined' && typeof indexedDB !== 'undefined') ? 1 : 0;
340 }) == 1;
341}
342
343// ROM Storage Operations
344absl::Status WasmStorage::SaveRom(const std::string& name, const std::vector<uint8_t>& data) {
345 EnsureInitialized();
346 if (!initialized_.load()) {
347 return absl::FailedPreconditionError("Storage not initialized");
348 }
349 int result = idb_save_binary(kRomStoreName, name.c_str(), data.data(), data.size());
350 if (result != 0) {
351 return absl::InternalError(absl::StrFormat("Failed to save ROM '%s'", name));
352 }
353 return absl::OkStatus();
354}
355
356absl::StatusOr<std::vector<uint8_t>> WasmStorage::LoadRom(const std::string& name) {
357 EnsureInitialized();
358 if (!initialized_.load()) {
359 return absl::FailedPreconditionError("Storage not initialized");
360 }
361 uint8_t* data_ptr = nullptr;
362 size_t data_size = 0;
363 int result = idb_load_binary(kRomStoreName, name.c_str(), &data_ptr, &data_size);
364 if (result == -2) {
365 if (data_ptr) free(data_ptr);
366 return absl::NotFoundError(absl::StrFormat("ROM '%s' not found", name));
367 } else if (result != 0) {
368 if (data_ptr) free(data_ptr);
369 return absl::InternalError(absl::StrFormat("Failed to load ROM '%s'", name));
370 }
371 std::vector<uint8_t> data(data_ptr, data_ptr + data_size);
372 free(data_ptr);
373 return data;
374}
375
376absl::Status WasmStorage::DeleteRom(const std::string& name) {
377 EnsureInitialized();
378 if (!initialized_.load()) {
379 return absl::FailedPreconditionError("Storage not initialized");
380 }
381 int result = idb_delete_entry(kRomStoreName, name.c_str());
382 if (result != 0) {
383 return absl::InternalError(absl::StrFormat("Failed to delete ROM '%s'", name));
384 }
385 return absl::OkStatus();
386}
387
388std::vector<std::string> WasmStorage::ListRoms() {
389 EnsureInitialized();
390 if (!initialized_.load()) {
391 return {};
392 }
393 char* keys_json = idb_list_keys(kRomStoreName);
394 if (!keys_json) {
395 return {};
396 }
397 std::vector<std::string> result;
398 try {
399 nlohmann::json keys = nlohmann::json::parse(keys_json);
400 for (const auto& key : keys) {
401 if (key.is_string()) {
402 result.push_back(key.get<std::string>());
403 }
404 }
405 } catch (const std::exception& e) {
406 emscripten_log(EM_LOG_ERROR, "Failed to parse ROM list: %s", e.what());
407 }
408 free(keys_json);
409 return result;
410}
411
412// Project Storage Operations
413absl::Status WasmStorage::SaveProject(const std::string& name, const std::string& json) {
414 EnsureInitialized();
415 if (!initialized_.load()) {
416 return absl::FailedPreconditionError("Storage not initialized");
417 }
418 int result = idb_save_string(kProjectStoreName, name.c_str(), json.c_str());
419 if (result != 0) {
420 return absl::InternalError(absl::StrFormat("Failed to save project '%s'", name));
421 }
422 return absl::OkStatus();
423}
424
425absl::StatusOr<std::string> WasmStorage::LoadProject(const std::string& name) {
426 EnsureInitialized();
427 if (!initialized_.load()) {
428 return absl::FailedPreconditionError("Storage not initialized");
429 }
430 char* json_ptr = idb_load_string(kProjectStoreName, name.c_str());
431 if (!json_ptr) {
432 // Note: idb_load_string returns 0 (null) on not found or error,
433 // no memory is allocated in that case, so no free needed here.
434 return absl::NotFoundError(absl::StrFormat("Project '%s' not found", name));
435 }
436 std::string json(json_ptr);
437 free(json_ptr);
438 return json;
439}
440
441absl::Status WasmStorage::DeleteProject(const std::string& name) {
442 EnsureInitialized();
443 if (!initialized_.load()) {
444 return absl::FailedPreconditionError("Storage not initialized");
445 }
446 int result = idb_delete_entry(kProjectStoreName, name.c_str());
447 if (result != 0) {
448 return absl::InternalError(absl::StrFormat("Failed to delete project '%s'", name));
449 }
450 return absl::OkStatus();
451}
452
453std::vector<std::string> WasmStorage::ListProjects() {
454 EnsureInitialized();
455 if (!initialized_.load()) {
456 return {};
457 }
458 char* keys_json = idb_list_keys(kProjectStoreName);
459 if (!keys_json) {
460 return {};
461 }
462 std::vector<std::string> result;
463 try {
464 nlohmann::json keys = nlohmann::json::parse(keys_json);
465 for (const auto& key : keys) {
466 if (key.is_string()) {
467 result.push_back(key.get<std::string>());
468 }
469 }
470 } catch (const std::exception& e) {
471 emscripten_log(EM_LOG_ERROR, "Failed to parse project list: %s", e.what());
472 }
473 free(keys_json);
474 return result;
475}
476
477// User Preferences Storage
478absl::Status WasmStorage::SavePreferences(const nlohmann::json& prefs) {
479 EnsureInitialized();
480 if (!initialized_.load()) {
481 return absl::FailedPreconditionError("Storage not initialized");
482 }
483 std::string json_str = prefs.dump();
484 int result = idb_save_string(kPreferencesStoreName, kPreferencesKey, json_str.c_str());
485 if (result != 0) {
486 return absl::InternalError("Failed to save preferences");
487 }
488 return absl::OkStatus();
489}
490
491absl::StatusOr<nlohmann::json> WasmStorage::LoadPreferences() {
492 EnsureInitialized();
493 if (!initialized_.load()) {
494 return absl::FailedPreconditionError("Storage not initialized");
495 }
496 char* json_ptr = idb_load_string(kPreferencesStoreName, kPreferencesKey);
497 if (!json_ptr) {
498 return nlohmann::json::object();
499 }
500 try {
501 nlohmann::json prefs = nlohmann::json::parse(json_ptr);
502 free(json_ptr);
503 return prefs;
504 } catch (const std::exception& e) {
505 free(json_ptr);
506 return absl::InvalidArgumentError(absl::StrFormat("Failed to parse preferences: %s", e.what()));
507 }
508}
509
510absl::Status WasmStorage::ClearPreferences() {
511 EnsureInitialized();
512 if (!initialized_.load()) {
513 return absl::FailedPreconditionError("Storage not initialized");
514 }
515 int result = idb_delete_entry(kPreferencesStoreName, kPreferencesKey);
516 if (result != 0) {
517 return absl::InternalError("Failed to clear preferences");
518 }
519 return absl::OkStatus();
520}
521
522// Utility Operations
523absl::StatusOr<size_t> WasmStorage::GetStorageUsage() {
524 EnsureInitialized();
525 if (!initialized_.load()) {
526 return absl::FailedPreconditionError("Storage not initialized");
527 }
528 return idb_get_storage_usage();
529}
530
531} // namespace platform
532} // namespace yaze
533
534#endif // __EMSCRIPTEN__
535// clang-format on
EM_JS(void, CallJsAiDriver,(const char *history_json), { if(window.yaze &&window.yaze.ai &&window.yaze.ai.processAgentRequest) { window.yaze.ai.processAgentRequest(UTF8ToString(history_json));} else { console.error("AI Driver not found in window.yaze.ai.processAgentRequest");} })