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