yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
wasm_autosave.cc
Go to the documentation of this file.
1#ifdef __EMSCRIPTEN__
2
4
5#include <emscripten.h>
6#include <emscripten/html5.h> // For emscripten_set_timeout/clear_timeout
7#include <chrono>
8#include <ctime>
9
10#include "absl/strings/str_format.h"
13
14namespace yaze {
15namespace platform {
16
17using ::yaze::app::platform::WasmConfig;
18
19// Static member initialization
20bool AutoSaveManager::emergency_save_triggered_ = false;
21
22// JavaScript event handler registration using EM_JS
23EM_JS(void, register_beforeunload_handler, (), {
24 // Store handler references for cleanup
25 if (!window._yazeAutoSaveHandlers) {
26 window._yazeAutoSaveHandlers = {};
27 }
28
29 // Register beforeunload event handler
30 window._yazeAutoSaveHandlers.beforeunload = function(event) {
31 // Call the C++ emergency save function
32 if (Module._yazeEmergencySave) {
33 Module._yazeEmergencySave();
34 }
35
36 // Some browsers require returnValue to be set
37 event.returnValue =
38 'You have unsaved changes. Are you sure you want to leave?';
39 return event.returnValue;
40 };
41 window.addEventListener('beforeunload',
42 window._yazeAutoSaveHandlers.beforeunload);
43
44 // Register visibilitychange event for when tab becomes hidden
45 window._yazeAutoSaveHandlers.visibilitychange = function() {
46 if (document.hidden && Module._yazeEmergencySave) {
47 // Save when tab becomes hidden (user switches tabs or minimizes)
48 Module._yazeEmergencySave();
49 }
50 };
51 document.addEventListener('visibilitychange',
52 window._yazeAutoSaveHandlers.visibilitychange);
53
54 // Register pagehide event as backup
55 window._yazeAutoSaveHandlers.pagehide = function(event) {
56 if (Module._yazeEmergencySave) {
57 Module._yazeEmergencySave();
58 }
59 };
60 window.addEventListener('pagehide', window._yazeAutoSaveHandlers.pagehide);
61
62 console.log('AutoSave event handlers registered');
63});
64
65EM_JS(void, unregister_beforeunload_handler, (), {
66 // Remove all event listeners using stored references
67 if (window._yazeAutoSaveHandlers) {
68 if (window._yazeAutoSaveHandlers.beforeunload) {
69 window.removeEventListener('beforeunload',
70 window._yazeAutoSaveHandlers.beforeunload);
71 }
72 if (window._yazeAutoSaveHandlers.visibilitychange) {
73 document.removeEventListener(
74 'visibilitychange', window._yazeAutoSaveHandlers.visibilitychange);
75 }
76 if (window._yazeAutoSaveHandlers.pagehide) {
77 window.removeEventListener('pagehide',
78 window._yazeAutoSaveHandlers.pagehide);
79 }
80 window._yazeAutoSaveHandlers = null;
81 }
82 console.log('AutoSave event handlers unregistered');
83});
84
85EM_JS(void, set_recovery_flag, (int has_recovery), {
86 try {
87 if (has_recovery) {
88 sessionStorage.setItem('yaze_has_recovery', 'true');
89 } else {
90 sessionStorage.removeItem('yaze_has_recovery');
91 }
92 } catch (e) {
93 console.error('Failed to set recovery flag:', e);
94 }
95});
96
97EM_JS(int, get_recovery_flag, (), {
98 try {
99 return sessionStorage.getItem('yaze_has_recovery') == 'true' ? 1 : 0;
100 } catch (e) {
101 console.error('Failed to get recovery flag:', e);
102 return 0;
103 }
104});
105
106// C functions exposed to JavaScript for emergency save and recovery
107extern "C" {
108EMSCRIPTEN_KEEPALIVE
109void yazeEmergencySave() {
110 if (!AutoSaveManager::emergency_save_triggered_) {
111 AutoSaveManager::emergency_save_triggered_ = true;
112 AutoSaveManager::Instance().EmergencySave();
113 }
114}
115
116EMSCRIPTEN_KEEPALIVE
117int yazeRecoverSession() {
118 auto status = AutoSaveManager::Instance().RecoverLastSession();
119 if (status.ok()) {
120 emscripten_log(EM_LOG_INFO, "Session recovery successful");
121 return 1;
122 } else {
123 emscripten_log(EM_LOG_WARN, "Session recovery failed: %s",
124 std::string(status.message()).c_str());
125 return 0;
126 }
127}
128
129EMSCRIPTEN_KEEPALIVE
130int yazeHasRecoveryData() {
131 return AutoSaveManager::Instance().HasRecoveryData() ? 1 : 0;
132}
133
134EMSCRIPTEN_KEEPALIVE
135void yazeClearRecoveryData() {
136 AutoSaveManager::Instance().ClearRecoveryData();
137}
138}
139
140// AutoSaveManager implementation
141
142AutoSaveManager::AutoSaveManager()
143 : interval_seconds_(
144 app::platform::WasmConfig::Get().autosave.interval_seconds),
145 enabled_(true),
146 running_(false),
147 timer_id_(-1),
148 save_count_(0),
149 error_count_(0),
150 recovery_count_(0),
151 event_handlers_initialized_(false) {
152 // Set the JavaScript function pointer
153 EM_ASM({
154 Module._yazeEmergencySave = Module.cwrap('yazeEmergencySave', null, []);
155 });
156}
157
158AutoSaveManager::~AutoSaveManager() {
159 Stop();
160 CleanupEventHandlers();
161}
162
163AutoSaveManager& AutoSaveManager::Instance() {
164 static AutoSaveManager instance;
165 return instance;
166}
167
168void AutoSaveManager::InitializeEventHandlers() {
169 if (!event_handlers_initialized_) {
170 register_beforeunload_handler();
171 event_handlers_initialized_ = true;
172 }
173}
174
175void AutoSaveManager::CleanupEventHandlers() {
176 if (event_handlers_initialized_) {
177 unregister_beforeunload_handler();
178 event_handlers_initialized_ = false;
179 }
180}
181
182void AutoSaveManager::TimerCallback(void* user_data) {
183 AutoSaveManager* manager = static_cast<AutoSaveManager*>(user_data);
184 if (manager && manager->IsRunning()) {
185 auto status = manager->PerformSave();
186 if (!status.ok()) {
187 emscripten_log(EM_LOG_WARN, "Auto-save failed: %s",
188 status.ToString().c_str());
189 }
190
191 // Reschedule the timer
192 if (manager->IsRunning()) {
193 manager->timer_id_ = emscripten_set_timeout(
194 TimerCallback, manager->interval_seconds_ * 1000, manager);
195 }
196 }
197}
198
199absl::Status AutoSaveManager::Start(int interval_seconds) {
200 std::lock_guard<std::mutex> lock(mutex_);
201
202 if (running_) {
203 return absl::AlreadyExistsError("Auto-save is already running");
204 }
205
206 interval_seconds_ = interval_seconds;
207 InitializeEventHandlers();
208
209 // Start the timer
210 timer_id_ =
211 emscripten_set_timeout(TimerCallback, interval_seconds_ * 1000, this);
212
213 running_ = true;
214 emscripten_log(EM_LOG_INFO, "Auto-save started with %d second interval",
215 interval_seconds);
216
217 return absl::OkStatus();
218}
219
220absl::Status AutoSaveManager::Stop() {
221 std::lock_guard<std::mutex> lock(mutex_);
222
223 if (!running_) {
224 return absl::OkStatus();
225 }
226
227 // Cancel the timer
228 if (timer_id_ >= 0) {
229 emscripten_clear_timeout(timer_id_);
230 timer_id_ = -1;
231 }
232
233 running_ = false;
234 emscripten_log(EM_LOG_INFO, "Auto-save stopped");
235
236 return absl::OkStatus();
237}
238
239bool AutoSaveManager::IsRunning() const {
240 std::lock_guard<std::mutex> lock(mutex_);
241 return running_;
242}
243
244void AutoSaveManager::RegisterComponent(const std::string& component_id,
245 SaveCallback save_fn,
246 RestoreCallback restore_fn) {
247 std::lock_guard<std::mutex> lock(mutex_);
248 components_[component_id] = {save_fn, restore_fn};
249 emscripten_log(EM_LOG_INFO, "Registered component for auto-save: %s",
250 component_id.c_str());
251}
252
253void AutoSaveManager::UnregisterComponent(const std::string& component_id) {
254 std::lock_guard<std::mutex> lock(mutex_);
255 components_.erase(component_id);
256 emscripten_log(EM_LOG_INFO, "Unregistered component from auto-save: %s",
257 component_id.c_str());
258}
259
260absl::Status AutoSaveManager::SaveNow() {
261 std::lock_guard<std::mutex> lock(mutex_);
262 return PerformSave();
263}
264
265nlohmann::json AutoSaveManager::CollectComponentData() {
266 nlohmann::json data = nlohmann::json::object();
267
268 for (const auto& [id, component] : components_) {
269 try {
270 if (component.save_fn) {
271 data[id] = component.save_fn();
272 }
273 } catch (const std::exception& e) {
274 emscripten_log(EM_LOG_ERROR, "Failed to save component %s: %s",
275 id.c_str(), e.what());
276 error_count_++;
277 }
278 }
279
280 return data;
281}
282
283absl::Status AutoSaveManager::PerformSave() {
284 nlohmann::json save_data = nlohmann::json::object();
285
286 // Collect data from all registered components
287 save_data["components"] = CollectComponentData();
288
289 // Add metadata
290 auto now = std::chrono::system_clock::now();
291 save_data["timestamp"] =
292 std::chrono::duration_cast<std::chrono::milliseconds>(
293 now.time_since_epoch())
294 .count();
295 save_data["version"] = 1;
296
297 // Save to storage
298 auto status = SaveToStorage(save_data);
299 if (status.ok()) {
300 last_save_time_ = now;
301 save_count_++;
302 set_recovery_flag(1); // Mark that recovery data is available
303 } else {
304 error_count_++;
305 }
306
307 return status;
308}
309
310absl::Status AutoSaveManager::SaveToStorage(const nlohmann::json& data) {
311 try {
312 // Convert JSON to string
313 std::string json_str = data.dump();
314
315 // Save to IndexedDB via WasmStorage
316 auto status = WasmStorage::SaveProject(kAutoSaveDataKey, json_str);
317 if (!status.ok()) {
318 return status;
319 }
320
321 // Save metadata separately for quick access
322 nlohmann::json meta;
323 meta["timestamp"] = data["timestamp"];
324 meta["component_count"] = data["components"].size();
325 meta["save_count"] = save_count_;
326
327 return WasmStorage::SaveProject(kAutoSaveMetaKey, meta.dump());
328 } catch (const std::exception& e) {
329 return absl::InternalError(
330 absl::StrFormat("Failed to save auto-save data: %s", e.what()));
331 }
332}
333
334absl::StatusOr<nlohmann::json> AutoSaveManager::LoadFromStorage() {
335 auto result = WasmStorage::LoadProject(kAutoSaveDataKey);
336 if (!result.ok()) {
337 return result.status();
338 }
339
340 try {
341 return nlohmann::json::parse(*result);
342 } catch (const std::exception& e) {
343 return absl::InvalidArgumentError(
344 absl::StrFormat("Failed to parse auto-save data: %s", e.what()));
345 }
346}
347
348void AutoSaveManager::EmergencySave() {
349 // Use try_lock to avoid blocking - emergency save should be fast
350 // If we can't get the lock, another operation is in progress
351 std::unique_lock<std::mutex> lock(mutex_, std::try_to_lock);
352
353 try {
354 nlohmann::json emergency_data = nlohmann::json::object();
355 emergency_data["emergency"] = true;
356
357 // Only collect component data if we got the lock
358 if (lock.owns_lock()) {
359 emergency_data["components"] = CollectComponentData();
360 } else {
361 // Can't safely access components, save empty state marker
362 emergency_data["components"] = nlohmann::json::object();
363 emergency_data["incomplete"] = true;
364 }
365
366 emergency_data["timestamp"] =
367 std::chrono::duration_cast<std::chrono::milliseconds>(
368 std::chrono::system_clock::now().time_since_epoch())
369 .count();
370
371 // Use synchronous localStorage for emergency save (faster than IndexedDB)
372 EM_ASM(
373 {
374 try {
375 const data = UTF8ToString($0);
376 localStorage.setItem('yaze_emergency_save', data);
377 console.log('Emergency save completed');
378 } catch (e) {
379 console.error('Emergency save failed:', e);
380 }
381 },
382 emergency_data.dump().c_str());
383
384 set_recovery_flag(1);
385 } catch (...) {
386 // Silently fail - we're in emergency mode
387 }
388}
389
390bool AutoSaveManager::HasRecoveryData() {
391 // Check both sessionStorage flag and actual data
392 if (get_recovery_flag() == 0) {
393 return false;
394 }
395
396 // Verify data actually exists
397 auto meta = WasmStorage::LoadProject(kAutoSaveMetaKey);
398 if (meta.ok()) {
399 return true;
400 }
401
402 // Check for emergency save data
403 int has_emergency = EM_ASM_INT(
404 { return localStorage.getItem('yaze_emergency_save') != null ? 1 : 0; });
405
406 return has_emergency == 1;
407}
408
409absl::StatusOr<nlohmann::json> AutoSaveManager::GetRecoveryInfo() {
410 // First try to get regular auto-save metadata
411 auto meta_result = WasmStorage::LoadProject(kAutoSaveMetaKey);
412 if (meta_result.ok()) {
413 try {
414 return nlohmann::json::parse(*meta_result);
415 } catch (...) {
416 // Continue to check emergency save
417 }
418 }
419
420 // Check for emergency save
421 char* emergency_data = nullptr;
422 EM_ASM(
423 {
424 const data = localStorage.getItem('yaze_emergency_save');
425 if (data) {
426 const len = lengthBytesUTF8(data) + 1;
427 const ptr = _malloc(len);
428 stringToUTF8(data, ptr, len);
429 setValue($0, ptr, 'i32');
430 }
431 },
432 &emergency_data);
433
434 if (emergency_data) {
435 try {
436 nlohmann::json data = nlohmann::json::parse(emergency_data);
437 free(emergency_data);
438
439 nlohmann::json info;
440 info["type"] = "emergency";
441 info["timestamp"] = data["timestamp"];
442 info["component_count"] = data["components"].size();
443 return info;
444 } catch (const std::exception& e) {
445 free(emergency_data);
446 return absl::InvalidArgumentError("Failed to parse emergency save data");
447 }
448 }
449
450 return absl::NotFoundError("No recovery data found");
451}
452
453absl::Status AutoSaveManager::RecoverLastSession() {
454 std::lock_guard<std::mutex> lock(mutex_);
455
456 // First try regular auto-save data
457 auto data_result = LoadFromStorage();
458 if (data_result.ok()) {
459 const auto& data = *data_result;
460 if (data.contains("components") && data["components"].is_object()) {
461 for (const auto& [id, component_data] : data["components"].items()) {
462 auto it = components_.find(id);
463 if (it != components_.end() && it->second.restore_fn) {
464 try {
465 it->second.restore_fn(component_data);
466 emscripten_log(EM_LOG_INFO, "Restored component: %s", id.c_str());
467 } catch (const std::exception& e) {
468 emscripten_log(EM_LOG_ERROR, "Failed to restore component %s: %s",
469 id.c_str(), e.what());
470 }
471 }
472 }
473 recovery_count_++;
474 set_recovery_flag(0); // Clear recovery flag
475 return absl::OkStatus();
476 }
477 }
478
479 // Try emergency save data
480 char* emergency_data = nullptr;
481 EM_ASM(
482 {
483 const data = localStorage.getItem('yaze_emergency_save');
484 if (data) {
485 const len = lengthBytesUTF8(data) + 1;
486 const ptr = _malloc(len);
487 stringToUTF8(data, ptr, len);
488 setValue($0, ptr, 'i32');
489 }
490 },
491 &emergency_data);
492
493 if (emergency_data) {
494 try {
495 nlohmann::json data = nlohmann::json::parse(emergency_data);
496 free(emergency_data);
497
498 if (data.contains("components") && data["components"].is_object()) {
499 for (const auto& [id, component_data] : data["components"].items()) {
500 auto it = components_.find(id);
501 if (it != components_.end() && it->second.restore_fn) {
502 try {
503 it->second.restore_fn(component_data);
504 emscripten_log(EM_LOG_INFO,
505 "Restored component from emergency save: %s",
506 id.c_str());
507 } catch (const std::exception& e) {
508 emscripten_log(EM_LOG_ERROR, "Failed to restore component %s: %s",
509 id.c_str(), e.what());
510 }
511 }
512 }
513
514 // Clear emergency save data
515 EM_ASM({ localStorage.removeItem('yaze_emergency_save'); });
516
517 recovery_count_++;
518 set_recovery_flag(0); // Clear recovery flag
519 return absl::OkStatus();
520 }
521 } catch (const std::exception& e) {
522 return absl::InvalidArgumentError(
523 absl::StrFormat("Failed to recover session: %s", e.what()));
524 }
525 }
526
527 return absl::NotFoundError("No recovery data available");
528}
529
530absl::Status AutoSaveManager::ClearRecoveryData() {
531 // Clear regular auto-save data
532 WasmStorage::DeleteProject(kAutoSaveDataKey);
533 WasmStorage::DeleteProject(kAutoSaveMetaKey);
534
535 // Clear emergency save data
536 EM_ASM({ localStorage.removeItem('yaze_emergency_save'); });
537
538 // Clear recovery flag
539 set_recovery_flag(0);
540
541 emscripten_log(EM_LOG_INFO, "Recovery data cleared");
542 return absl::OkStatus();
543}
544
545void AutoSaveManager::SetInterval(int seconds) {
546 bool was_running = IsRunning();
547 if (was_running) {
548 Stop();
549 }
550
551 interval_seconds_ = seconds;
552
553 if (was_running && enabled_) {
554 Start(seconds);
555 }
556}
557
558void AutoSaveManager::SetEnabled(bool enabled) {
559 enabled_ = enabled;
560 if (!enabled && IsRunning()) {
561 Stop();
562 } else if (enabled && !IsRunning()) {
563 Start(interval_seconds_);
564 }
565}
566
567nlohmann::json AutoSaveManager::GetStatistics() const {
568 std::lock_guard<std::mutex> lock(mutex_);
569
570 nlohmann::json stats;
571 stats["save_count"] = save_count_;
572 stats["error_count"] = error_count_;
573 stats["recovery_count"] = recovery_count_;
574 stats["components_registered"] = components_.size();
575 stats["is_running"] = running_;
576 stats["interval_seconds"] = interval_seconds_;
577 stats["enabled"] = enabled_;
578
579 if (last_save_time_.time_since_epoch().count() > 0) {
580 stats["last_save_timestamp"] =
581 std::chrono::duration_cast<std::chrono::milliseconds>(
582 last_save_time_.time_since_epoch())
583 .count();
584 }
585
586 return stats;
587}
588
589} // namespace platform
590} // namespace yaze
591
592#endif // __EMSCRIPTEN__
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");} })