yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
wasm_collaboration.cc
Go to the documentation of this file.
1#ifdef __EMSCRIPTEN__
2
4
5#include <emscripten.h>
6#include <emscripten/bind.h>
7#include <emscripten/val.h>
8
9#include <chrono>
10#include <cmath>
11#include <random>
12#include <sstream>
13
14#include "absl/strings/str_format.h"
16#include "nlohmann/json.hpp"
17
18using json = nlohmann::json;
19
20// clang-format off
21EM_JS(double, GetCurrentTime, (), {
22 return Date.now() / 1000.0;
23});
24
25EM_JS(void, ConsoleLog, (const char* message), {
26 console.log('[WasmCollaboration] ' + UTF8ToString(message));
27});
28
29EM_JS(void, ConsoleError, (const char* message), {
30 console.error('[WasmCollaboration] ' + UTF8ToString(message));
31});
32
33EM_JS(void, UpdateCollaborationUI, (const char* type, const char* data), {
34 if (typeof window.updateCollaborationUI === 'function') {
35 window.updateCollaborationUI(UTF8ToString(type), UTF8ToString(data));
36 }
37});
38
39EM_JS(char*, GenerateRandomRoomCode, (), {
40 var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
41 var result = '';
42 for (var i = 0; i < 6; i++) {
43 result += chars.charAt(Math.floor(Math.random() * chars.length));
44 }
45 var lengthBytes = lengthBytesUTF8(result) + 1;
46 var stringOnWasmHeap = _malloc(lengthBytes);
47 stringToUTF8(result, stringOnWasmHeap, lengthBytes);
48 return stringOnWasmHeap;
49});
50
51EM_JS(char*, GetCollaborationServerUrl, (), {
52 // Check for configuration in order of precedence:
53 // 1. window.YAZE_CONFIG.collaborationServerUrl
54 // 2. Environment variable via meta tag
55 // 3. Default empty (disabled)
56 var url = '';
57
58 if (typeof window !== 'undefined') {
59 if (window.YAZE_CONFIG && window.YAZE_CONFIG.collaborationServerUrl) {
60 url = window.YAZE_CONFIG.collaborationServerUrl;
61 } else {
62 // Check for meta tag configuration
63 var meta = document.querySelector('meta[name="yaze-collab-server"]');
64 if (meta && meta.content) {
65 url = meta.content;
66 }
67 }
68 }
69
70 if (url.length === 0) {
71 return null;
72 }
73
74 var lengthBytes = lengthBytesUTF8(url) + 1;
75 var stringOnWasmHeap = _malloc(lengthBytes);
76 stringToUTF8(url, stringOnWasmHeap, lengthBytes);
77 return stringOnWasmHeap;
78});
79// clang-format on
80
81namespace yaze {
82namespace app {
83namespace platform {
84
85namespace {
86
87// Color palette for user cursors
88const std::vector<std::string> kUserColors = {
89 "#FF6B6B", // Red
90 "#4ECDC4", // Teal
91 "#45B7D1", // Blue
92 "#96CEB4", // Green
93 "#FFEAA7", // Yellow
94 "#DDA0DD", // Plum
95 "#98D8C8", // Mint
96 "#F7DC6F", // Gold
97};
98
99WasmCollaboration& GetInstance() {
100 static WasmCollaboration instance;
101 return instance;
102}
103
104} // namespace
105
106WasmCollaboration& GetWasmCollaborationInstance() {
107 return GetInstance();
108}
109
110WasmCollaboration::WasmCollaboration() {
111 user_id_ = GenerateUserId();
112 user_color_ = GenerateUserColor();
113 websocket_ = std::make_unique<net::EmscriptenWebSocket>();
114
115 // Try to initialize from config automatically
116 InitializeFromConfig();
117}
118
119WasmCollaboration::~WasmCollaboration() {
120 if (is_connected_) {
121 LeaveSession();
122 }
123}
124
125void WasmCollaboration::InitializeFromConfig() {
126 char* url = GetCollaborationServerUrl();
127 if (url != nullptr) {
128 websocket_url_ = std::string(url);
129 free(url);
130 ConsoleLog(("Collaboration server configured: " + websocket_url_).c_str());
131 } else {
132 ConsoleLog(
133 "Collaboration server not configured. Set "
134 "window.YAZE_CONFIG.collaborationServerUrl or add <meta "
135 "name=\"yaze-collab-server\" content=\"wss://...\"> to enable.");
136 }
137}
138
139absl::StatusOr<std::string> WasmCollaboration::CreateSession(
140 const std::string& session_name, const std::string& username,
141 const std::string& password) {
142 if (is_connected_ || connection_state_ == ConnectionState::Connecting) {
143 return absl::FailedPreconditionError(
144 "Already connected or connecting to a session");
145 }
146
147 if (!IsConfigured()) {
148 return absl::FailedPreconditionError(
149 "Collaboration server not configured. Set "
150 "window.YAZE_CONFIG.collaborationServerUrl "
151 "or call SetWebSocketUrl() before creating a session.");
152 }
153
154 // Generate room code
155 char* room_code_ptr = GenerateRandomRoomCode();
156 room_code_ = std::string(room_code_ptr);
157 free(room_code_ptr);
158
159 session_name_ = session_name;
160 username_ = username;
161 stored_password_ = password;
162 should_reconnect_ = true; // Enable auto-reconnect for this session
163
164 UpdateConnectionState(ConnectionState::Connecting, "Creating session...");
165
166 // Connect to WebSocket server
167 auto status = websocket_->Connect(websocket_url_);
168 if (!status.ok()) {
169 return status;
170 }
171
172 // Set up WebSocket callbacks
173 websocket_->OnOpen([this, password]() {
174 ConsoleLog("WebSocket connected, creating session");
175 is_connected_ = true;
176 UpdateConnectionState(ConnectionState::Connected, "Connected");
177
178 // Add self to users list
179 User self_user;
180 self_user.id = user_id_;
181 self_user.name = username_;
182 self_user.color = user_color_;
183 self_user.is_active = true;
184 self_user.last_activity = GetCurrentTime();
185
186 {
187 std::lock_guard<std::mutex> lock(users_mutex_);
188 users_[user_id_] = self_user;
189 }
190
191 // Send create session message
192 json msg;
193 msg["type"] = "create";
194 msg["room"] = room_code_;
195 msg["name"] = session_name_;
196 msg["user"] = username_;
197 msg["user_id"] = user_id_;
198 msg["color"] = user_color_;
199 if (!password.empty()) {
200 msg["password"] = password;
201 }
202
203 auto send_status = websocket_->Send(msg.dump());
204 if (!send_status.ok()) {
205 ConsoleError("Failed to send create message");
206 }
207
208 if (status_callback_) {
209 status_callback_(true, "Session created");
210 }
211 UpdateCollaborationUI("session_created", room_code_.c_str());
212 });
213
214 websocket_->OnMessage(
215 [this](const std::string& message) { HandleMessage(message); });
216
217 websocket_->OnClose([this](int code, const std::string& reason) {
218 is_connected_ = false;
219 ConsoleLog(absl::StrFormat("WebSocket closed: %s (code: %d)", reason, code)
220 .c_str());
221
222 // Initiate reconnection if enabled
223 if (should_reconnect_) {
224 InitiateReconnection();
225 } else {
226 UpdateConnectionState(ConnectionState::Disconnected,
227 absl::StrFormat("Disconnected: %s", reason));
228 }
229
230 if (status_callback_) {
231 status_callback_(false, absl::StrFormat("Disconnected: %s", reason));
232 }
233 UpdateCollaborationUI("disconnected", "");
234 });
235
236 websocket_->OnError([this](const std::string& error) {
237 ConsoleError(error.c_str());
238 is_connected_ = false;
239
240 // Initiate reconnection on error
241 if (should_reconnect_) {
242 InitiateReconnection();
243 } else {
244 UpdateConnectionState(ConnectionState::Disconnected, error);
245 }
246
247 if (status_callback_) {
248 status_callback_(false, error);
249 }
250 });
251
252 // Note: is_connected_ will be set to true in OnOpen callback when connection is established
253 // For now, mark as "connecting" state by returning the room code
254 // The actual connected state is confirmed in HandleMessage when create_response is received
255
256 UpdateCollaborationUI("session_creating", room_code_.c_str());
257 return room_code_;
258}
259
260absl::Status WasmCollaboration::JoinSession(const std::string& room_code,
261 const std::string& username,
262 const std::string& password) {
263 if (is_connected_ || connection_state_ == ConnectionState::Connecting) {
264 return absl::FailedPreconditionError(
265 "Already connected or connecting to a session");
266 }
267
268 if (!IsConfigured()) {
269 return absl::FailedPreconditionError(
270 "Collaboration server not configured. Set "
271 "window.YAZE_CONFIG.collaborationServerUrl "
272 "or call SetWebSocketUrl() before joining a session.");
273 }
274
275 room_code_ = room_code;
276 username_ = username;
277 stored_password_ = password;
278 should_reconnect_ = true; // Enable auto-reconnect for this session
279
280 UpdateConnectionState(ConnectionState::Connecting, "Joining session...");
281
282 // Connect to WebSocket server
283 auto status = websocket_->Connect(websocket_url_);
284 if (!status.ok()) {
285 return status;
286 }
287
288 // Set up WebSocket callbacks
289 websocket_->OnOpen([this, password]() {
290 ConsoleLog("WebSocket connected, joining session");
291 is_connected_ = true;
292 UpdateConnectionState(ConnectionState::Connected, "Connected");
293
294 // Send join session message
295 json msg;
296 msg["type"] = "join";
297 msg["room"] = room_code_;
298 msg["user"] = username_;
299 msg["user_id"] = user_id_;
300 msg["color"] = user_color_;
301 if (!password.empty()) {
302 msg["password"] = password;
303 }
304
305 auto send_status = websocket_->Send(msg.dump());
306 if (!send_status.ok()) {
307 ConsoleError("Failed to send join message");
308 }
309
310 if (status_callback_) {
311 status_callback_(true, "Joined session");
312 }
313 UpdateCollaborationUI("session_joined", room_code_.c_str());
314 });
315
316 websocket_->OnMessage(
317 [this](const std::string& message) { HandleMessage(message); });
318
319 websocket_->OnClose([this](int code, const std::string& reason) {
320 is_connected_ = false;
321 ConsoleLog(absl::StrFormat("WebSocket closed: %s (code: %d)", reason, code)
322 .c_str());
323
324 // Initiate reconnection if enabled
325 if (should_reconnect_) {
326 InitiateReconnection();
327 } else {
328 UpdateConnectionState(ConnectionState::Disconnected,
329 absl::StrFormat("Disconnected: %s", reason));
330 }
331
332 if (status_callback_) {
333 status_callback_(false, absl::StrFormat("Disconnected: %s", reason));
334 }
335 UpdateCollaborationUI("disconnected", "");
336 });
337
338 websocket_->OnError([this](const std::string& error) {
339 ConsoleError(error.c_str());
340 is_connected_ = false;
341
342 // Initiate reconnection on error
343 if (should_reconnect_) {
344 InitiateReconnection();
345 } else {
346 UpdateConnectionState(ConnectionState::Disconnected, error);
347 }
348
349 if (status_callback_) {
350 status_callback_(false, error);
351 }
352 });
353
354 // Note: is_connected_ will be set in OnOpen callback
355 UpdateCollaborationUI("session_joining", room_code_.c_str());
356 return absl::OkStatus();
357}
358
359absl::Status WasmCollaboration::LeaveSession() {
360 if (!is_connected_ && connection_state_ != ConnectionState::Connecting &&
361 connection_state_ != ConnectionState::Reconnecting) {
362 return absl::FailedPreconditionError("Not connected to a session");
363 }
364
365 // Disable auto-reconnect when explicitly leaving
366 should_reconnect_ = false;
367
368 // Send leave message if connected
369 if (is_connected_) {
370 json msg;
371 msg["type"] = "leave";
372 msg["room"] = room_code_;
373 msg["user_id"] = user_id_;
374
375 auto status = websocket_->Send(msg.dump());
376 if (!status.ok()) {
377 ConsoleError("Failed to send leave message");
378 }
379 }
380
381 // Close WebSocket connection
382 if (websocket_) {
383 websocket_->Close();
384 }
385 is_connected_ = false;
386 UpdateConnectionState(ConnectionState::Disconnected, "Left session");
387
388 // Clear state
389 room_code_.clear();
390 session_name_.clear();
391 stored_password_.clear();
392 ResetReconnectionState();
393
394 {
395 std::lock_guard<std::mutex> lock(users_mutex_);
396 users_.clear();
397 }
398
399 {
400 std::lock_guard<std::mutex> lock(cursors_mutex_);
401 cursors_.clear();
402 }
403
404 if (status_callback_) {
405 status_callback_(false, "Left session");
406 }
407
408 UpdateCollaborationUI("session_left", "");
409 return absl::OkStatus();
410}
411
413 uint32_t offset, const std::vector<uint8_t>& old_data,
414 const std::vector<uint8_t>& new_data) {
416 if (old_data.size() > max_size || new_data.size() > max_size) {
417 return absl::InvalidArgumentError(
418 absl::StrFormat("Change size exceeds maximum of %d bytes", max_size));
419 }
420
421 // Create change message
422 json msg;
423 msg["type"] = "change";
424 msg["room"] = room_code_;
425 msg["user_id"] = user_id_;
426 msg["offset"] = offset;
427 msg["old_data"] = old_data;
428 msg["new_data"] = new_data;
429 msg["timestamp"] = GetCurrentTime();
430
431 std::string message = msg.dump();
432
433 // If disconnected, queue the message for later
434 if (!is_connected_) {
435 if (connection_state_ == ConnectionState::Reconnecting) {
436 QueueMessageWhileDisconnected(message);
437 return absl::OkStatus(); // Queued successfully
438 } else {
439 return absl::FailedPreconditionError("Not connected to a session");
440 }
441 }
442
443 auto status = websocket_->Send(message);
444 if (!status.ok()) {
445 // Try to queue on send failure
446 if (connection_state_ == ConnectionState::Reconnecting) {
447 QueueMessageWhileDisconnected(message);
448 return absl::OkStatus();
449 }
450 return absl::InternalError("Failed to send change");
451 }
452
453 UpdateUserActivity(user_id_);
454 return absl::OkStatus();
455}
456
457absl::Status WasmCollaboration::SendCursorPosition(
458 const std::string& editor_type, int x, int y, int map_id) {
459 // Don't queue cursor updates during reconnection - they're transient
460 if (!is_connected_) {
461 if (connection_state_ == ConnectionState::Reconnecting) {
462 return absl::OkStatus(); // Silently drop during reconnection
463 }
464 return absl::FailedPreconditionError("Not connected to a session");
465 }
466
467 // Rate limit cursor updates
468 double now = GetCurrentTime();
469 double cursor_interval =
471 if (now - last_cursor_send_ < cursor_interval) {
472 return absl::OkStatus(); // Silently skip
473 }
474 last_cursor_send_ = now;
475
476 // Send cursor update
477 json msg;
478 msg["type"] = "cursor";
479 msg["room"] = room_code_;
480 msg["user_id"] = user_id_;
481 msg["editor"] = editor_type;
482 msg["x"] = x;
483 msg["y"] = y;
484 if (map_id >= 0) {
485 msg["map_id"] = map_id;
486 }
487
488 auto status = websocket_->Send(msg.dump());
489 if (!status.ok()) {
490 // Don't fail on cursor send errors during reconnection
491 if (connection_state_ == ConnectionState::Reconnecting) {
492 return absl::OkStatus();
493 }
494 return absl::InternalError("Failed to send cursor position");
495 }
496
497 UpdateUserActivity(user_id_);
498 return absl::OkStatus();
499}
500
501std::vector<WasmCollaboration::User> WasmCollaboration::GetConnectedUsers()
502 const {
503 std::lock_guard<std::mutex> lock(users_mutex_);
504 std::vector<User> result;
505 for (const auto& [id, user] : users_) {
506 if (user.is_active) {
507 result.push_back(user);
508 }
509 }
510 return result;
511}
512
514 return is_connected_ && websocket_ && websocket_->IsConnected();
515}
516
517void WasmCollaboration::ProcessPendingChanges() {
518 std::vector<ChangeEvent> changes_to_apply;
519
520 {
521 std::lock_guard<std::mutex> lock(changes_mutex_);
522 changes_to_apply = std::move(pending_changes_);
523 pending_changes_.clear();
524 }
525
526 for (const auto& change : changes_to_apply) {
527 if (IsChangeValid(change)) {
528 ApplyRemoteChange(change);
529 }
530 }
531
532 // Check for user timeouts
533 CheckUserTimeouts();
534}
535
536void WasmCollaboration::HandleMessage(const std::string& message) {
537 try {
538 json msg = json::parse(message);
539 std::string type = msg["type"];
540
541 if (type == "create_response") {
542 // Session created successfully
543 if (msg["success"]) {
544 session_name_ = msg["session_name"];
545 ConsoleLog("Session created successfully");
546 } else {
547 ConsoleError(msg["error"].get<std::string>().c_str());
548 is_connected_ = false;
549 }
550 } else if (type == "join_response") {
551 // Joined session successfully
552 if (msg["success"]) {
553 session_name_ = msg["session_name"];
554 ConsoleLog("Joined session successfully");
555 } else {
556 ConsoleError(msg["error"].get<std::string>().c_str());
557 is_connected_ = false;
558 }
559 } else if (type == "users") {
560 // User list update
561 std::lock_guard<std::mutex> lock(users_mutex_);
562 users_.clear();
563
564 for (const auto& user_data : msg["list"]) {
565 User user;
566 user.id = user_data["id"];
567 user.name = user_data["name"];
568 user.color = user_data["color"];
569 user.is_active = user_data["active"];
570 user.last_activity = GetCurrentTime();
571 users_[user.id] = user;
572 }
573
574 if (user_list_callback_) {
575 user_list_callback_(GetConnectedUsers());
576 }
577
578 // Update UI with user list
579 json ui_data;
580 ui_data["users"] = msg["list"];
581 UpdateCollaborationUI("users_update", ui_data.dump().c_str());
582
583 } else if (type == "change") {
584 // ROM change from another user
585 if (msg["user_id"] != user_id_) { // Don't process our own changes
586 ChangeEvent change;
587 change.offset = msg["offset"];
588 change.old_data = msg["old_data"].get<std::vector<uint8_t>>();
589 change.new_data = msg["new_data"].get<std::vector<uint8_t>>();
590 change.user_id = msg["user_id"];
591 change.timestamp = msg["timestamp"];
592
593 {
594 std::lock_guard<std::mutex> lock(changes_mutex_);
595 pending_changes_.push_back(change);
596 }
597
598 UpdateUserActivity(change.user_id);
599 }
600 } else if (type == "cursor") {
601 // Cursor position update
602 if (msg["user_id"] != user_id_) { // Don't process our own cursor
603 CursorInfo cursor;
604 cursor.user_id = msg["user_id"];
605 cursor.editor_type = msg["editor"];
606 cursor.x = msg["x"];
607 cursor.y = msg["y"];
608 if (msg.contains("map_id")) {
609 cursor.map_id = msg["map_id"];
610 }
611
612 {
613 std::lock_guard<std::mutex> lock(cursors_mutex_);
614 cursors_[cursor.user_id] = cursor;
615 }
616
617 if (cursor_callback_) {
618 cursor_callback_(cursor);
619 }
620
621 // Update UI with cursor position
622 json ui_data;
623 ui_data["user_id"] = cursor.user_id;
624 ui_data["editor"] = cursor.editor_type;
625 ui_data["x"] = cursor.x;
626 ui_data["y"] = cursor.y;
627 UpdateCollaborationUI("cursor_update", ui_data.dump().c_str());
628
629 UpdateUserActivity(cursor.user_id);
630 }
631 } else if (type == "error") {
632 ConsoleError(msg["message"].get<std::string>().c_str());
633 if (status_callback_) {
634 status_callback_(false, msg["message"]);
635 }
636 }
637 } catch (const json::exception& e) {
638 ConsoleError(absl::StrFormat("JSON parse error: %s", e.what()).c_str());
639 }
640}
641
642std::string WasmCollaboration::GenerateUserId() {
643 std::random_device rd;
644 std::mt19937 gen(rd());
645 std::uniform_int_distribution<> dis(0, 15);
646
647 std::stringstream ss;
648 ss << "user_";
649 for (int i = 0; i < 8; ++i) {
650 ss << std::hex << dis(gen);
651 }
652 return ss.str();
653}
654
655std::string WasmCollaboration::GenerateUserColor() {
656 std::random_device rd;
657 std::mt19937 gen(rd());
658 std::uniform_int_distribution<> dis(0, kUserColors.size() - 1);
659 return kUserColors[dis(gen)];
660}
661
662void WasmCollaboration::UpdateUserActivity(const std::string& user_id) {
663 std::lock_guard<std::mutex> lock(users_mutex_);
664 if (users_.find(user_id) != users_.end()) {
665 users_[user_id].last_activity = GetCurrentTime();
666 users_[user_id].is_active = true;
667 }
668}
669
670void WasmCollaboration::CheckUserTimeouts() {
671 double now = GetCurrentTime();
672 std::lock_guard<std::mutex> lock(users_mutex_);
673
675 bool users_changed = false;
676 for (auto& [id, user] : users_) {
677 if (user.is_active && (now - user.last_activity) > timeout) {
678 user.is_active = false;
679 users_changed = true;
680 }
681 }
682
683 if (users_changed && user_list_callback_) {
684 user_list_callback_(GetConnectedUsers());
685 }
686}
687
688bool WasmCollaboration::IsChangeValid(const ChangeEvent& change) {
689 // Validate change doesn't exceed ROM bounds
690 if (!rom_) {
691 return false;
692 }
693
694 if (change.offset + change.new_data.size() > rom_->size()) {
695 ConsoleError(
696 absl::StrFormat("Change at offset %u exceeds ROM size", change.offset)
697 .c_str());
698 return false;
699 }
700
701 // Could add more validation here (e.g., check if area is editable)
702 return true;
703}
704
705void WasmCollaboration::ApplyRemoteChange(const ChangeEvent& change) {
706 if (!rom_) {
707 ConsoleError("ROM not set, cannot apply changes");
708 return;
709 }
710
711 applying_remote_change_ = true;
712 // Apply the change to the ROM
713 for (size_t i = 0; i < change.new_data.size(); ++i) {
714 rom_->WriteByte(change.offset + i, change.new_data[i]);
715 }
716 applying_remote_change_ = false;
717
718 // Notify the UI about the change
719 if (change_callback_) {
720 change_callback_(change);
721 }
722
723 // Update UI with change info
724 json ui_data;
725 ui_data["offset"] = change.offset;
726 ui_data["size"] = change.new_data.size();
727 ui_data["user_id"] = change.user_id;
728 UpdateCollaborationUI("change_applied", ui_data.dump().c_str());
729}
730
731void WasmCollaboration::UpdateConnectionState(ConnectionState new_state,
732 const std::string& message) {
733 connection_state_ = new_state;
734
735 // Notify via callback
736 if (connection_state_callback_) {
737 connection_state_callback_(new_state, message);
738 }
739
740 // Update UI
741 std::string state_str;
742 switch (new_state) {
743 case ConnectionState::Disconnected:
744 state_str = "disconnected";
745 break;
746 case ConnectionState::Connecting:
747 state_str = "connecting";
748 break;
749 case ConnectionState::Connected:
750 state_str = "connected";
751 break;
752 case ConnectionState::Reconnecting:
753 state_str = "reconnecting";
754 break;
755 }
756
757 json ui_data;
758 ui_data["state"] = state_str;
759 ui_data["message"] = message;
760 UpdateCollaborationUI("connection_state", ui_data.dump().c_str());
761}
762
763void WasmCollaboration::InitiateReconnection() {
764 if (!should_reconnect_ || room_code_.empty()) {
765 UpdateConnectionState(ConnectionState::Disconnected, "Disconnected");
766 return;
767 }
768
769 if (reconnection_attempts_ >= max_reconnection_attempts_) {
770 ConsoleError(
771 absl::StrFormat("Max reconnection attempts reached (%d), giving up",
772 max_reconnection_attempts_)
773 .c_str());
774 UpdateConnectionState(ConnectionState::Disconnected,
775 "Reconnection failed - max attempts reached");
776 ResetReconnectionState();
777 return;
778 }
779
780 reconnection_attempts_++;
781 UpdateConnectionState(
782 ConnectionState::Reconnecting,
783 absl::StrFormat("Reconnecting... (attempt %d/%d)", reconnection_attempts_,
784 max_reconnection_attempts_));
785
786 // Calculate delay with exponential backoff
787 double delay = std::min(
788 reconnection_delay_seconds_ * std::pow(2, reconnection_attempts_ - 1),
789 max_reconnection_delay_);
790
791 ConsoleLog(absl::StrFormat("Will reconnect in %.1f seconds (attempt %d)",
792 delay, reconnection_attempts_)
793 .c_str());
794
795 // Schedule reconnection using emscripten_set_timeout
796 emscripten_async_call(
797 [](void* arg) {
798 WasmCollaboration* self = static_cast<WasmCollaboration*>(arg);
799 self->AttemptReconnection();
800 },
801 this, delay * 1000); // Convert to milliseconds
802}
803
804void WasmCollaboration::AttemptReconnection() {
805 if (is_connected_ || connection_state_ == ConnectionState::Connected) {
806 // Already reconnected somehow
807 ResetReconnectionState();
808 return;
809 }
810
811 ConsoleLog(absl::StrFormat("Attempting to reconnect to room %s", room_code_)
812 .c_str());
813
814 // Create new websocket instance
815 websocket_ = std::make_unique<net::EmscriptenWebSocket>();
816
817 // Attempt connection
818 auto status = websocket_->Connect(websocket_url_);
819 if (!status.ok()) {
820 ConsoleError(
821 absl::StrFormat("Reconnection failed: %s", status.message()).c_str());
822 InitiateReconnection(); // Schedule next attempt
823 return;
824 }
825
826 // Set up WebSocket callbacks for reconnection
827 websocket_->OnOpen([this]() {
828 ConsoleLog("WebSocket reconnected, rejoining session");
829 is_connected_ = true;
830 UpdateConnectionState(ConnectionState::Connected,
831 "Reconnected successfully");
832
833 // Send rejoin message
834 json msg;
835 msg["type"] = "join";
836 msg["room"] = room_code_;
837 msg["user"] = username_;
838 msg["user_id"] = user_id_;
839 msg["color"] = user_color_;
840 if (!stored_password_.empty()) {
841 msg["password"] = stored_password_;
842 }
843 msg["rejoin"] = true; // Indicate this is a reconnection
844
845 auto send_status = websocket_->Send(msg.dump());
846 if (!send_status.ok()) {
847 ConsoleError("Failed to send rejoin message");
848 }
849
850 // Reset reconnection state on success
851 ResetReconnectionState();
852
853 // Send any queued messages
854 std::vector<std::string> messages_to_send;
855 {
856 std::lock_guard<std::mutex> lock(message_queue_mutex_);
857 messages_to_send = std::move(queued_messages_);
858 queued_messages_.clear();
859 }
860
861 for (const auto& msg : messages_to_send) {
862 websocket_->Send(msg);
863 }
864
865 if (status_callback_) {
866 status_callback_(true, "Reconnected to session");
867 }
868 UpdateCollaborationUI("session_reconnected", room_code_.c_str());
869 });
870
871 websocket_->OnMessage(
872 [this](const std::string& message) { HandleMessage(message); });
873
874 websocket_->OnClose([this](int code, const std::string& reason) {
875 is_connected_ = false;
876 ConsoleLog(
877 absl::StrFormat("Reconnection WebSocket closed: %s", reason).c_str());
878
879 // Attempt reconnection again
880 InitiateReconnection();
881
882 if (status_callback_) {
883 status_callback_(false, absl::StrFormat("Disconnected: %s", reason));
884 }
885 });
886
887 websocket_->OnError([this](const std::string& error) {
888 ConsoleError(absl::StrFormat("Reconnection error: %s", error).c_str());
889 is_connected_ = false;
890
891 // Attempt reconnection again
892 InitiateReconnection();
893
894 if (status_callback_) {
895 status_callback_(false, error);
896 }
897 });
898}
899
900void WasmCollaboration::ResetReconnectionState() {
901 reconnection_attempts_ = 0;
902 reconnection_delay_seconds_ = 1.0; // Reset to initial delay
903}
904
905void WasmCollaboration::QueueMessageWhileDisconnected(
906 const std::string& message) {
907 std::lock_guard<std::mutex> lock(message_queue_mutex_);
908
909 // Limit queue size to prevent memory issues
910 if (queued_messages_.size() >= max_queued_messages_) {
911 ConsoleLog("Message queue full, dropping oldest message");
912 queued_messages_.erase(queued_messages_.begin());
913 }
914
915 queued_messages_.push_back(message);
916 ConsoleLog(absl::StrFormat("Queued message for reconnection (queue size: %d)",
917 queued_messages_.size())
918 .c_str());
919}
920
921// ---------------------------------------------------------------------------
922// JS bindings for WASM (exported with EMSCRIPTEN_KEEPALIVE)
923// ---------------------------------------------------------------------------
924extern "C" {
925
926EMSCRIPTEN_KEEPALIVE const char* WasmCollaborationCreate(
927 const char* session_name, const char* username, const char* password) {
928 static std::string last_room_code;
929 if (!session_name || !username) {
930 ConsoleError("Invalid session/user parameters");
931 return nullptr;
932 }
933 auto& collab = GetInstance();
934 auto result = collab.CreateSession(session_name, username,
935 password ? std::string(password) : "");
936 if (!result.ok()) {
937 ConsoleError(std::string(result.status().message()).c_str());
938 return nullptr;
939 }
940 last_room_code = *result;
941 return last_room_code.c_str();
942}
943
944EMSCRIPTEN_KEEPALIVE int WasmCollaborationJoin(const char* room_code,
945 const char* username,
946 const char* password) {
947 if (!room_code || !username) {
948 ConsoleError("room_code and username are required");
949 return 0;
950 }
951 auto& collab = GetInstance();
952 auto status = collab.JoinSession(room_code, username,
953 password ? std::string(password) : "");
954 if (!status.ok()) {
955 ConsoleError(std::string(status.message()).c_str());
956 return 0;
957 }
958 return 1;
959}
960
961EMSCRIPTEN_KEEPALIVE int WasmCollaborationLeave() {
962 auto& collab = GetInstance();
963 auto status = collab.LeaveSession();
964 return status.ok() ? 1 : 0;
965}
966
967EMSCRIPTEN_KEEPALIVE int WasmCollaborationSendCursor(const char* editor_type,
968 int x, int y, int map_id) {
969 auto& collab = GetInstance();
970 auto status = collab.SendCursorPosition(editor_type ? editor_type : "unknown",
971 x, y, map_id);
972 return status.ok() ? 1 : 0;
973}
974
975EMSCRIPTEN_KEEPALIVE int WasmCollaborationBroadcastChange(
976 uint32_t offset, const uint8_t* new_data, size_t length) {
977 if (!new_data && length > 0) {
978 return 0;
979 }
980 auto& collab = GetInstance();
981 std::vector<uint8_t> data;
982 data.reserve(length);
983 for (size_t i = 0; i < length; ++i) {
984 data.push_back(new_data[i]);
985 }
986 std::vector<uint8_t> old_data; // Not tracked in WASM path
987 auto status = collab.BroadcastChange(offset, old_data, data);
988 return status.ok() ? 1 : 0;
989}
990
991EMSCRIPTEN_KEEPALIVE void WasmCollaborationSetServerUrl(const char* url) {
992 if (!url)
993 return;
994 auto& collab = GetInstance();
995 collab.SetWebSocketUrl(std::string(url));
996}
997
998EMSCRIPTEN_KEEPALIVE int WasmCollaborationIsConnected() {
999 return GetInstance().IsConnected() ? 1 : 0;
1000}
1001
1002EMSCRIPTEN_KEEPALIVE const char* WasmCollaborationGetRoomCode() {
1003 static std::string room;
1004 room = GetInstance().GetRoomCode();
1005 return room.c_str();
1006}
1007
1008EMSCRIPTEN_KEEPALIVE const char* WasmCollaborationGetUserId() {
1009 static std::string user;
1010 user = GetInstance().GetUserId();
1011 return user.c_str();
1012}
1013
1014} // extern "C"
1015
1016} // namespace platform
1017} // namespace app
1018} // namespace yaze
1019
1020#endif // __EMSCRIPTEN__
absl::Status JoinSession(const std::string &, const std::string &)
std::vector< User > GetConnectedUsers() const
absl::Status BroadcastChange(uint32_t, const std::vector< uint8_t > &, const std::vector< uint8_t > &)
absl::StatusOr< std::string > CreateSession(const std::string &, const std::string &)
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");} })
static WasmConfig & Get()
struct yaze::app::platform::WasmConfig::Collaboration collaboration