6#include <emscripten/bind.h>
7#include <emscripten/val.h>
14#include "absl/strings/str_format.h"
15#include "nlohmann/json.hpp"
18using json = nlohmann::json;
21EM_JS(
double, GetCurrentTime, (), {
22 return Date.now() / 1000.0;
25EM_JS(
void, ConsoleLog, (
const char* message), {
26 console.log(
'[WasmCollaboration] ' + UTF8ToString(message));
29EM_JS(
void, ConsoleError, (
const char* message), {
30 console.error(
'[WasmCollaboration] ' + UTF8ToString(message));
33EM_JS(
void, UpdateCollaborationUI, (
const char* type,
const char* data), {
34 if (typeof window.updateCollaborationUI ===
'function') {
35 window.updateCollaborationUI(UTF8ToString(type), UTF8ToString(data));
39EM_JS(
char*, GenerateRandomRoomCode, (), {
40 var chars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
42 for (var i = 0; i < 6; i++) {
43 result += chars.charAt(Math.floor(Math.random() * chars.length));
45 var lengthBytes = lengthBytesUTF8(result) + 1;
46 var stringOnWasmHeap = _malloc(lengthBytes);
47 stringToUTF8(result, stringOnWasmHeap, lengthBytes);
48 return stringOnWasmHeap;
51EM_JS(
char*, GetCollaborationServerUrl, (), {
58 if (typeof window !==
'undefined') {
59 if (window.YAZE_CONFIG && window.YAZE_CONFIG.collaborationServerUrl) {
60 url = window.YAZE_CONFIG.collaborationServerUrl;
63 var meta = document.querySelector(
'meta[name="yaze-collab-server"]');
64 if (meta && meta.content) {
70 if (url.length === 0) {
74 var lengthBytes = lengthBytesUTF8(url) + 1;
75 var stringOnWasmHeap = _malloc(lengthBytes);
76 stringToUTF8(url, stringOnWasmHeap, lengthBytes);
77 return stringOnWasmHeap;
88const std::vector<std::string> kUserColors = {
99WasmCollaboration& GetInstance() {
100 static WasmCollaboration instance;
106WasmCollaboration& GetWasmCollaborationInstance() {
return GetInstance(); }
108WasmCollaboration::WasmCollaboration() {
109 user_id_ = GenerateUserId();
110 user_color_ = GenerateUserColor();
111 websocket_ = std::make_unique<net::EmscriptenWebSocket>();
114 InitializeFromConfig();
117WasmCollaboration::~WasmCollaboration() {
123void WasmCollaboration::InitializeFromConfig() {
124 char* url = GetCollaborationServerUrl();
125 if (url !=
nullptr) {
126 websocket_url_ = std::string(url);
128 ConsoleLog((
"Collaboration server configured: " + websocket_url_).c_str());
130 ConsoleLog(
"Collaboration server not configured. Set window.YAZE_CONFIG.collaborationServerUrl or add <meta name=\"yaze-collab-server\" content=\"wss://...\"> to enable.");
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");
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.");
148 char* room_code_ptr = GenerateRandomRoomCode();
149 room_code_ = std::string(room_code_ptr);
152 session_name_ = session_name;
153 username_ = username;
154 stored_password_ = password;
155 should_reconnect_ =
true;
157 UpdateConnectionState(ConnectionState::Connecting,
"Creating session...");
160 auto status = websocket_->Connect(websocket_url_);
166 websocket_->OnOpen([
this, password]() {
167 ConsoleLog(
"WebSocket connected, creating session");
168 is_connected_ =
true;
169 UpdateConnectionState(ConnectionState::Connected,
"Connected");
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();
180 std::lock_guard<std::mutex> lock(users_mutex_);
181 users_[user_id_] = self_user;
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;
196 auto send_status = websocket_->Send(msg.dump());
197 if (!send_status.ok()) {
198 ConsoleError(
"Failed to send create message");
201 if (status_callback_) {
202 status_callback_(
true,
"Session created");
204 UpdateCollaborationUI(
"session_created", room_code_.c_str());
207 websocket_->OnMessage([
this](
const std::string& message) {
208 HandleMessage(message);
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());
216 if (should_reconnect_) {
217 InitiateReconnection();
219 UpdateConnectionState(ConnectionState::Disconnected, absl::StrFormat(
"Disconnected: %s", reason));
222 if (status_callback_) {
223 status_callback_(
false, absl::StrFormat(
"Disconnected: %s", reason));
225 UpdateCollaborationUI(
"disconnected",
"");
228 websocket_->OnError([
this](
const std::string& error) {
229 ConsoleError(error.c_str());
230 is_connected_ =
false;
233 if (should_reconnect_) {
234 InitiateReconnection();
236 UpdateConnectionState(ConnectionState::Disconnected, error);
239 if (status_callback_) {
240 status_callback_(
false, error);
248 UpdateCollaborationUI(
"session_creating", room_code_.c_str());
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");
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.");
265 room_code_ = room_code;
266 username_ = username;
267 stored_password_ = password;
268 should_reconnect_ =
true;
270 UpdateConnectionState(ConnectionState::Connecting,
"Joining session...");
273 auto status = websocket_->Connect(websocket_url_);
279 websocket_->OnOpen([
this, password]() {
280 ConsoleLog(
"WebSocket connected, joining session");
281 is_connected_ =
true;
282 UpdateConnectionState(ConnectionState::Connected,
"Connected");
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;
295 auto send_status = websocket_->Send(msg.dump());
296 if (!send_status.ok()) {
297 ConsoleError(
"Failed to send join message");
300 if (status_callback_) {
301 status_callback_(
true,
"Joined session");
303 UpdateCollaborationUI(
"session_joined", room_code_.c_str());
306 websocket_->OnMessage([
this](
const std::string& message) {
307 HandleMessage(message);
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());
315 if (should_reconnect_) {
316 InitiateReconnection();
318 UpdateConnectionState(ConnectionState::Disconnected, absl::StrFormat(
"Disconnected: %s", reason));
321 if (status_callback_) {
322 status_callback_(
false, absl::StrFormat(
"Disconnected: %s", reason));
324 UpdateCollaborationUI(
"disconnected",
"");
327 websocket_->OnError([
this](
const std::string& error) {
328 ConsoleError(error.c_str());
329 is_connected_ =
false;
332 if (should_reconnect_) {
333 InitiateReconnection();
335 UpdateConnectionState(ConnectionState::Disconnected, error);
338 if (status_callback_) {
339 status_callback_(
false, error);
344 UpdateCollaborationUI(
"session_joining", room_code_.c_str());
345 return absl::OkStatus();
349 if (!is_connected_ && connection_state_ != ConnectionState::Connecting &&
350 connection_state_ != ConnectionState::Reconnecting) {
351 return absl::FailedPreconditionError(
"Not connected to a session");
355 should_reconnect_ =
false;
360 msg[
"type"] =
"leave";
361 msg[
"room"] = room_code_;
362 msg[
"user_id"] = user_id_;
364 auto status = websocket_->Send(msg.dump());
366 ConsoleError(
"Failed to send leave message");
374 is_connected_ =
false;
375 UpdateConnectionState(ConnectionState::Disconnected,
"Left session");
379 session_name_.clear();
380 stored_password_.clear();
381 ResetReconnectionState();
384 std::lock_guard<std::mutex> lock(users_mutex_);
389 std::lock_guard<std::mutex> lock(cursors_mutex_);
393 if (status_callback_) {
394 status_callback_(
false,
"Left session");
397 UpdateCollaborationUI(
"session_left",
"");
398 return absl::OkStatus();
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));
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();
420 std::string message = msg.dump();
423 if (!is_connected_) {
424 if (connection_state_ == ConnectionState::Reconnecting) {
425 QueueMessageWhileDisconnected(message);
426 return absl::OkStatus();
428 return absl::FailedPreconditionError(
"Not connected to a session");
432 auto status = websocket_->Send(message);
435 if (connection_state_ == ConnectionState::Reconnecting) {
436 QueueMessageWhileDisconnected(message);
437 return absl::OkStatus();
439 return absl::InternalError(
"Failed to send change");
442 UpdateUserActivity(user_id_);
443 return absl::OkStatus();
446absl::Status WasmCollaboration::SendCursorPosition(
447 const std::string& editor_type,
int x,
int y,
int map_id) {
449 if (!is_connected_) {
450 if (connection_state_ == ConnectionState::Reconnecting) {
451 return absl::OkStatus();
453 return absl::FailedPreconditionError(
"Not connected to a session");
457 double now = GetCurrentTime();
459 if (now - last_cursor_send_ < cursor_interval) {
460 return absl::OkStatus();
462 last_cursor_send_ = now;
466 msg[
"type"] =
"cursor";
467 msg[
"room"] = room_code_;
468 msg[
"user_id"] = user_id_;
469 msg[
"editor"] = editor_type;
473 msg[
"map_id"] = map_id;
476 auto status = websocket_->Send(msg.dump());
479 if (connection_state_ == ConnectionState::Reconnecting) {
480 return absl::OkStatus();
482 return absl::InternalError(
"Failed to send cursor position");
485 UpdateUserActivity(user_id_);
486 return absl::OkStatus();
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);
501 return is_connected_ && websocket_ && websocket_->IsConnected();
504void WasmCollaboration::ProcessPendingChanges() {
505 std::vector<ChangeEvent> changes_to_apply;
508 std::lock_guard<std::mutex> lock(changes_mutex_);
509 changes_to_apply = std::move(pending_changes_);
510 pending_changes_.clear();
513 for (
const auto& change : changes_to_apply) {
514 if (IsChangeValid(change)) {
515 ApplyRemoteChange(change);
523void WasmCollaboration::HandleMessage(
const std::string& message) {
525 json msg = json::parse(message);
526 std::string type = msg[
"type"];
528 if (type ==
"create_response") {
530 if (msg[
"success"]) {
531 session_name_ = msg[
"session_name"];
532 ConsoleLog(
"Session created successfully");
534 ConsoleError(msg[
"error"].get<std::string>().c_str());
535 is_connected_ =
false;
537 }
else if (type ==
"join_response") {
539 if (msg[
"success"]) {
540 session_name_ = msg[
"session_name"];
541 ConsoleLog(
"Joined session successfully");
543 ConsoleError(msg[
"error"].get<std::string>().c_str());
544 is_connected_ =
false;
546 }
else if (type ==
"users") {
548 std::lock_guard<std::mutex> lock(users_mutex_);
551 for (
const auto& user_data : msg[
"list"]) {
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;
561 if (user_list_callback_) {
567 ui_data[
"users"] = msg[
"list"];
568 UpdateCollaborationUI(
"users_update", ui_data.dump().c_str());
570 }
else if (type ==
"change") {
572 if (msg[
"user_id"] != user_id_) {
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"];
581 std::lock_guard<std::mutex> lock(changes_mutex_);
582 pending_changes_.push_back(change);
585 UpdateUserActivity(change.user_id);
587 }
else if (type ==
"cursor") {
589 if (msg[
"user_id"] != user_id_) {
591 cursor.user_id = msg[
"user_id"];
592 cursor.editor_type = msg[
"editor"];
595 if (msg.contains(
"map_id")) {
596 cursor.map_id = msg[
"map_id"];
600 std::lock_guard<std::mutex> lock(cursors_mutex_);
601 cursors_[cursor.user_id] = cursor;
604 if (cursor_callback_) {
605 cursor_callback_(cursor);
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());
616 UpdateUserActivity(cursor.user_id);
618 }
else if (type ==
"error") {
619 ConsoleError(msg[
"message"].get<std::string>().c_str());
620 if (status_callback_) {
621 status_callback_(
false, msg[
"message"]);
624 }
catch (
const json::exception& e) {
625 ConsoleError(absl::StrFormat(
"JSON parse error: %s", e.what()).c_str());
629std::string WasmCollaboration::GenerateUserId() {
630 std::random_device rd;
631 std::mt19937 gen(rd());
632 std::uniform_int_distribution<> dis(0, 15);
634 std::stringstream ss;
636 for (
int i = 0; i < 8; ++i) {
637 ss << std::hex << dis(gen);
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)];
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;
657void WasmCollaboration::CheckUserTimeouts() {
658 double now = GetCurrentTime();
659 std::lock_guard<std::mutex> lock(users_mutex_);
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;
670 if (users_changed && user_list_callback_) {
675bool WasmCollaboration::IsChangeValid(
const ChangeEvent& change) {
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());
691void WasmCollaboration::ApplyRemoteChange(
const ChangeEvent& change) {
693 ConsoleError(
"ROM not set, cannot apply changes");
697 applying_remote_change_ =
true;
699 for (
size_t i = 0; i < change.new_data.size(); ++i) {
700 rom_->WriteByte(change.offset + i, change.new_data[i]);
702 applying_remote_change_ =
false;
705 if (change_callback_) {
706 change_callback_(change);
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());
717void WasmCollaboration::UpdateConnectionState(ConnectionState new_state,
const std::string& message) {
718 connection_state_ = new_state;
721 if (connection_state_callback_) {
722 connection_state_callback_(new_state, message);
726 std::string state_str;
728 case ConnectionState::Disconnected:
729 state_str =
"disconnected";
731 case ConnectionState::Connecting:
732 state_str =
"connecting";
734 case ConnectionState::Connected:
735 state_str =
"connected";
737 case ConnectionState::Reconnecting:
738 state_str =
"reconnecting";
743 ui_data[
"state"] = state_str;
744 ui_data[
"message"] = message;
745 UpdateCollaborationUI(
"connection_state", ui_data.dump().c_str());
748void WasmCollaboration::InitiateReconnection() {
749 if (!should_reconnect_ || room_code_.empty()) {
750 UpdateConnectionState(ConnectionState::Disconnected,
"Disconnected");
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();
762 reconnection_attempts_++;
763 UpdateConnectionState(ConnectionState::Reconnecting,
764 absl::StrFormat(
"Reconnecting... (attempt %d/%d)",
765 reconnection_attempts_, max_reconnection_attempts_));
768 double delay = std::min(reconnection_delay_seconds_ * std::pow(2, reconnection_attempts_ - 1),
769 max_reconnection_delay_);
771 ConsoleLog(absl::StrFormat(
"Will reconnect in %.1f seconds (attempt %d)",
772 delay, reconnection_attempts_).c_str());
775 emscripten_async_call([](
void* arg) {
776 WasmCollaboration* self =
static_cast<WasmCollaboration*
>(arg);
777 self->AttemptReconnection();
778 },
this, delay * 1000);
781void WasmCollaboration::AttemptReconnection() {
782 if (is_connected_ || connection_state_ == ConnectionState::Connected) {
784 ResetReconnectionState();
788 ConsoleLog(absl::StrFormat(
"Attempting to reconnect to room %s", room_code_).c_str());
791 websocket_ = std::make_unique<net::EmscriptenWebSocket>();
794 auto status = websocket_->Connect(websocket_url_);
796 ConsoleError(absl::StrFormat(
"Reconnection failed: %s", status.message()).c_str());
797 InitiateReconnection();
802 websocket_->OnOpen([
this]() {
803 ConsoleLog(
"WebSocket reconnected, rejoining session");
804 is_connected_ =
true;
805 UpdateConnectionState(ConnectionState::Connected,
"Reconnected successfully");
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_;
817 msg[
"rejoin"] =
true;
819 auto send_status = websocket_->Send(msg.dump());
820 if (!send_status.ok()) {
821 ConsoleError(
"Failed to send rejoin message");
825 ResetReconnectionState();
828 std::vector<std::string> messages_to_send;
830 std::lock_guard<std::mutex> lock(message_queue_mutex_);
831 messages_to_send = std::move(queued_messages_);
832 queued_messages_.clear();
835 for (
const auto& msg : messages_to_send) {
836 websocket_->Send(msg);
839 if (status_callback_) {
840 status_callback_(
true,
"Reconnected to session");
842 UpdateCollaborationUI(
"session_reconnected", room_code_.c_str());
845 websocket_->OnMessage([
this](
const std::string& message) {
846 HandleMessage(message);
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());
854 InitiateReconnection();
856 if (status_callback_) {
857 status_callback_(
false, absl::StrFormat(
"Disconnected: %s", reason));
861 websocket_->OnError([
this](
const std::string& error) {
862 ConsoleError(absl::StrFormat(
"Reconnection error: %s", error).c_str());
863 is_connected_ =
false;
866 InitiateReconnection();
868 if (status_callback_) {
869 status_callback_(
false, error);
874void WasmCollaboration::ResetReconnectionState() {
875 reconnection_attempts_ = 0;
876 reconnection_delay_seconds_ = 1.0;
879void WasmCollaboration::QueueMessageWhileDisconnected(
const std::string& message) {
880 std::lock_guard<std::mutex> lock(message_queue_mutex_);
883 if (queued_messages_.size() >= max_queued_messages_) {
884 ConsoleLog(
"Message queue full, dropping oldest message");
885 queued_messages_.erase(queued_messages_.begin());
888 queued_messages_.push_back(message);
889 ConsoleLog(absl::StrFormat(
"Queued message for reconnection (queue size: %d)",
890 queued_messages_.size()).c_str());
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");
905 auto& collab = GetInstance();
906 auto result = collab.CreateSession(session_name, username,
907 password ? std::string(password) :
"");
909 ConsoleError(std::string(result.status().message()).c_str());
912 last_room_code = *result;
913 return last_room_code.c_str();
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");
923 auto& collab = GetInstance();
924 auto status = collab.JoinSession(room_code, username,
925 password ? std::string(password) :
"");
927 ConsoleError(std::string(status.message()).c_str());
933EMSCRIPTEN_KEEPALIVE
int WasmCollaborationLeave() {
934 auto& collab = GetInstance();
935 auto status = collab.LeaveSession();
936 return status.ok() ? 1 : 0;
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",
944 return status.ok() ? 1 : 0;
947EMSCRIPTEN_KEEPALIVE
int WasmCollaborationBroadcastChange(
948 uint32_t offset,
const uint8_t* new_data,
size_t length) {
949 if (!new_data && length > 0) {
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]);
958 std::vector<uint8_t> old_data;
959 auto status = collab.BroadcastChange(offset, old_data, data);
960 return status.ok() ? 1 : 0;
963EMSCRIPTEN_KEEPALIVE
void WasmCollaborationSetServerUrl(
const char* url) {
965 auto& collab = GetInstance();
966 collab.SetWebSocketUrl(std::string(url));
969EMSCRIPTEN_KEEPALIVE
int WasmCollaborationIsConnected() {
970 return GetInstance().IsConnected() ? 1 : 0;
973EMSCRIPTEN_KEEPALIVE
const char* WasmCollaborationGetRoomCode() {
974 static std::string room;
975 room = GetInstance().GetRoomCode();
979EMSCRIPTEN_KEEPALIVE
const char* WasmCollaborationGetUserId() {
980 static std::string user;
981 user = GetInstance().GetUserId();
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");} })