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