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