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, const char* value), {
139 const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
140 return asyncify.handleAsync(function() {
141 var storeName = UTF8ToString(store_name);
142 var keyStr = UTF8ToString(key);
143 var valueStr = UTF8ToString(value);
144 var operation = function() {
145 return new Promise(function(resolve, reject) {
146 if (!Module._yazeDB) {
147 console.error('Database not initialized');
148 resolve(-1);
149 return;
150 }
151 var transaction = Module._yazeDB.transaction([storeName], 'readwrite');
152 var store = transaction.objectStore(storeName);
153 var request = store.put(valueStr, keyStr);
154 request.onsuccess = function() { resolve(0); };
155 request.onerror = function() {
156 console.error('Failed to save string:', request.error);
157 resolve(-1);
158 };
159 });
160 };
161 if (window.yazeAsyncQueue) {
162 return window.yazeAsyncQueue.enqueue(operation);
163 }
164 return operation();
165 });
166});
167
168EM_JS(char*, idb_load_string, (const char* store_name, const char* key), {
169 const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
170 return asyncify.handleAsync(function() {
171 if (!Module._yazeDB) {
172 console.error('Database not initialized');
173 return 0;
174 }
175 var storeName = UTF8ToString(store_name);
176 var keyStr = UTF8ToString(key);
177 var operation = function() {
178 return new Promise(function(resolve) {
179 var transaction = Module._yazeDB.transaction([storeName], 'readonly');
180 var store = transaction.objectStore(storeName);
181 var request = store.get(keyStr);
182 request.onsuccess = function() {
183 var result = request.result;
184 if (result && typeof result === 'string') {
185 var len = lengthBytesUTF8(result) + 1;
186 var ptr = Module._malloc(len);
187 stringToUTF8(result, ptr, len);
188 resolve(ptr);
189 } else {
190 resolve(0);
191 }
192 };
193 request.onerror = function() {
194 console.error('Failed to load string:', request.error);
195 resolve(0);
196 };
197 });
198 };
199 if (window.yazeAsyncQueue) {
200 return window.yazeAsyncQueue.enqueue(operation);
201 }
202 return operation();
203 });
204});
205
206EM_JS(int, idb_delete_entry, (const char* store_name, const char* key), {
207 const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
208 return asyncify.handleAsync(function() {
209 var storeName = UTF8ToString(store_name);
210 var keyStr = UTF8ToString(key);
211 var operation = function() {
212 return new Promise(function(resolve, reject) {
213 if (!Module._yazeDB) {
214 console.error('Database not initialized');
215 resolve(-1);
216 return;
217 }
218 var transaction = Module._yazeDB.transaction([storeName], 'readwrite');
219 var store = transaction.objectStore(storeName);
220 var request = store.delete(keyStr);
221 request.onsuccess = function() { resolve(0); };
222 request.onerror = function() {
223 console.error('Failed to delete entry:', request.error);
224 resolve(-1);
225 };
226 });
227 };
228 if (window.yazeAsyncQueue) {
229 return window.yazeAsyncQueue.enqueue(operation);
230 }
231 return operation();
232 });
233});
234
235EM_JS(char*, idb_list_keys, (const char* store_name), {
236 const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
237 return asyncify.handleAsync(function() {
238 if (!Module._yazeDB) {
239 console.error('Database not initialized');
240 return 0;
241 }
242 var storeName = UTF8ToString(store_name);
243 var operation = function() {
244 return new Promise(function(resolve) {
245 var transaction = Module._yazeDB.transaction([storeName], 'readonly');
246 var store = transaction.objectStore(storeName);
247 var request = store.getAllKeys();
248 request.onsuccess = function() {
249 var keys = request.result;
250 var jsonStr = JSON.stringify(keys);
251 var len = lengthBytesUTF8(jsonStr) + 1;
252 var ptr = Module._malloc(len);
253 stringToUTF8(jsonStr, ptr, len);
254 resolve(ptr);
255 };
256 request.onerror = function() {
257 console.error('Failed to list keys:', request.error);
258 resolve(0);
259 };
260 });
261 };
262 if (window.yazeAsyncQueue) {
263 return window.yazeAsyncQueue.enqueue(operation);
264 }
265 return operation();
266 });
267});
268
269EM_JS(size_t, idb_get_storage_usage, (), {
270 const asyncify = typeof Asyncify !== 'undefined' ? Asyncify : Module.Asyncify;
271 return asyncify.handleAsync(function() {
272 if (!Module._yazeDB) {
273 console.error('Database not initialized');
274 return 0;
275 }
276 var operation = function() {
277 return new Promise(function(resolve) {
278 var totalSize = 0;
279 var storeNames = ['roms', 'projects', 'preferences'];
280 var completed = 0;
281
282 storeNames.forEach(function(storeName) {
283 var transaction = Module._yazeDB.transaction([storeName], 'readonly');
284 var store = transaction.objectStore(storeName);
285 var request = store.openCursor();
286
287 request.onsuccess = function(event) {
288 var cursor = event.target.result;
289 if (cursor) {
290 var value = cursor.value;
291 if (value instanceof Uint8Array) {
292 totalSize += value.byteLength;
293 } else if (value instanceof ArrayBuffer) {
294 totalSize += value.byteLength;
295 } else if (value && ArrayBuffer.isView(value)) {
296 totalSize += value.byteLength;
297 } else if (typeof value === 'string') {
298 totalSize += value.length * 2; // UTF-16 estimation
299 } else if (value) {
300 totalSize += JSON.stringify(value).length * 2;
301 }
302 cursor.continue();
303 } else {
304 completed++;
305 if (completed === storeNames.length) {
306 resolve(totalSize);
307 }
308 }
309 };
310
311 request.onerror = function() {
312 completed++;
313 if (completed === storeNames.length) {
314 resolve(totalSize);
315 }
316 };
317 });
318 });
319 };
320 if (window.yazeAsyncQueue) {
321 return window.yazeAsyncQueue.enqueue(operation);
322 }
323 return operation();
324 });
325});
326
327// Implementation of WasmStorage methods
328absl::Status WasmStorage::Initialize() {
329 // Use compare_exchange for thread-safe initialization
330 bool expected = false;
331 if (!initialized_.compare_exchange_strong(expected, true)) {
332 return absl::OkStatus(); // Already initialized by another thread
333 }
334
335 int result = idb_open_database(kDatabaseName, kDatabaseVersion);
336 if (result != 0) {
337 initialized_.store(false); // Reset on failure
338 return absl::InternalError("Failed to initialize IndexedDB");
339 }
340 return absl::OkStatus();
341}
342
343void WasmStorage::EnsureInitialized() {
344 if (!initialized_.load()) {
345 auto status = Initialize();
346 if (!status.ok()) {
347 emscripten_log(EM_LOG_ERROR, "Failed to initialize WasmStorage: %s", status.ToString().c_str());
348 }
349 }
350}
351
352bool WasmStorage::IsStorageAvailable() {
353 EnsureInitialized();
354 return initialized_.load();
355}
356
357bool WasmStorage::IsWebContext() {
358 return EM_ASM_INT({
359 return (typeof window !== 'undefined' && typeof indexedDB !== 'undefined') ? 1 : 0;
360 }) == 1;
361}
362
363// ROM Storage Operations
364absl::Status WasmStorage::SaveRom(const std::string& name, const std::vector<uint8_t>& data) {
365 EnsureInitialized();
366 if (!initialized_.load()) {
367 return absl::FailedPreconditionError("Storage not initialized");
368 }
369 int result = idb_save_binary(kRomStoreName, name.c_str(), data.data(), data.size());
370 if (result != 0) {
371 return absl::InternalError(absl::StrFormat("Failed to save ROM '%s'", name));
372 }
373 return absl::OkStatus();
374}
375
376absl::StatusOr<std::vector<uint8_t>> WasmStorage::LoadRom(const std::string& name) {
377 EnsureInitialized();
378 if (!initialized_.load()) {
379 return absl::FailedPreconditionError("Storage not initialized");
380 }
381 uint8_t* data_ptr = nullptr;
382 size_t data_size = 0;
383 int result = idb_load_binary(kRomStoreName, name.c_str(), &data_ptr, &data_size);
384 if (result == -2) {
385 if (data_ptr) free(data_ptr);
386 return absl::NotFoundError(absl::StrFormat("ROM '%s' not found", name));
387 } else if (result != 0) {
388 if (data_ptr) free(data_ptr);
389 return absl::InternalError(absl::StrFormat("Failed to load ROM '%s'", name));
390 }
391 std::vector<uint8_t> data(data_ptr, data_ptr + data_size);
392 free(data_ptr);
393 return data;
394}
395
396absl::Status WasmStorage::DeleteRom(const std::string& name) {
397 EnsureInitialized();
398 if (!initialized_.load()) {
399 return absl::FailedPreconditionError("Storage not initialized");
400 }
401 int result = idb_delete_entry(kRomStoreName, name.c_str());
402 if (result != 0) {
403 return absl::InternalError(absl::StrFormat("Failed to delete ROM '%s'", name));
404 }
405 return absl::OkStatus();
406}
407
408std::vector<std::string> WasmStorage::ListRoms() {
409 EnsureInitialized();
410 if (!initialized_.load()) {
411 return {};
412 }
413 char* keys_json = idb_list_keys(kRomStoreName);
414 if (!keys_json) {
415 return {};
416 }
417 std::vector<std::string> result;
418 try {
419 nlohmann::json keys = nlohmann::json::parse(keys_json);
420 for (const auto& key : keys) {
421 if (key.is_string()) {
422 result.push_back(key.get<std::string>());
423 }
424 }
425 } catch (const std::exception& e) {
426 emscripten_log(EM_LOG_ERROR, "Failed to parse ROM list: %s", e.what());
427 }
428 free(keys_json);
429 return result;
430}
431
432// Project Storage Operations
433absl::Status WasmStorage::SaveProject(const std::string& name, const std::string& json) {
434 EnsureInitialized();
435 if (!initialized_.load()) {
436 return absl::FailedPreconditionError("Storage not initialized");
437 }
438 int result = idb_save_string(kProjectStoreName, name.c_str(), json.c_str());
439 if (result != 0) {
440 return absl::InternalError(absl::StrFormat("Failed to save project '%s'", name));
441 }
442 return absl::OkStatus();
443}
444
445absl::StatusOr<std::string> WasmStorage::LoadProject(const std::string& name) {
446 EnsureInitialized();
447 if (!initialized_.load()) {
448 return absl::FailedPreconditionError("Storage not initialized");
449 }
450 char* json_ptr = idb_load_string(kProjectStoreName, name.c_str());
451 if (!json_ptr) {
452 // Note: idb_load_string returns 0 (null) on not found or error,
453 // no memory is allocated in that case, so no free needed here.
454 return absl::NotFoundError(absl::StrFormat("Project '%s' not found", name));
455 }
456 std::string json(json_ptr);
457 free(json_ptr);
458 return json;
459}
460
461absl::Status WasmStorage::DeleteProject(const std::string& name) {
462 EnsureInitialized();
463 if (!initialized_.load()) {
464 return absl::FailedPreconditionError("Storage not initialized");
465 }
466 int result = idb_delete_entry(kProjectStoreName, name.c_str());
467 if (result != 0) {
468 return absl::InternalError(absl::StrFormat("Failed to delete project '%s'", name));
469 }
470 return absl::OkStatus();
471}
472
473std::vector<std::string> WasmStorage::ListProjects() {
474 EnsureInitialized();
475 if (!initialized_.load()) {
476 return {};
477 }
478 char* keys_json = idb_list_keys(kProjectStoreName);
479 if (!keys_json) {
480 return {};
481 }
482 std::vector<std::string> result;
483 try {
484 nlohmann::json keys = nlohmann::json::parse(keys_json);
485 for (const auto& key : keys) {
486 if (key.is_string()) {
487 result.push_back(key.get<std::string>());
488 }
489 }
490 } catch (const std::exception& e) {
491 emscripten_log(EM_LOG_ERROR, "Failed to parse project list: %s", e.what());
492 }
493 free(keys_json);
494 return result;
495}
496
497// User Preferences Storage
498absl::Status WasmStorage::SavePreferences(const nlohmann::json& prefs) {
499 EnsureInitialized();
500 if (!initialized_.load()) {
501 return absl::FailedPreconditionError("Storage not initialized");
502 }
503 std::string json_str = prefs.dump();
504 int result = idb_save_string(kPreferencesStoreName, kPreferencesKey, json_str.c_str());
505 if (result != 0) {
506 return absl::InternalError("Failed to save preferences");
507 }
508 return absl::OkStatus();
509}
510
511absl::StatusOr<nlohmann::json> WasmStorage::LoadPreferences() {
512 EnsureInitialized();
513 if (!initialized_.load()) {
514 return absl::FailedPreconditionError("Storage not initialized");
515 }
516 char* json_ptr = idb_load_string(kPreferencesStoreName, kPreferencesKey);
517 if (!json_ptr) {
518 return nlohmann::json::object();
519 }
520 try {
521 nlohmann::json prefs = nlohmann::json::parse(json_ptr);
522 free(json_ptr);
523 return prefs;
524 } catch (const std::exception& e) {
525 free(json_ptr);
526 return absl::InvalidArgumentError(absl::StrFormat("Failed to parse preferences: %s", e.what()));
527 }
528}
529
530absl::Status WasmStorage::ClearPreferences() {
531 EnsureInitialized();
532 if (!initialized_.load()) {
533 return absl::FailedPreconditionError("Storage not initialized");
534 }
535 int result = idb_delete_entry(kPreferencesStoreName, kPreferencesKey);
536 if (result != 0) {
537 return absl::InternalError("Failed to clear preferences");
538 }
539 return absl::OkStatus();
540}
541
542// Utility Operations
543absl::StatusOr<size_t> WasmStorage::GetStorageUsage() {
544 EnsureInitialized();
545 if (!initialized_.load()) {
546 return absl::FailedPreconditionError("Storage not initialized");
547 }
548 return idb_get_storage_usage();
549}
550
551} // namespace platform
552} // namespace yaze
553
554#endif // __EMSCRIPTEN__
555// 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");} })