yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
websocket_client.cc
Go to the documentation of this file.
2
3#include <chrono>
4#include <mutex>
5#include <thread>
6
7#include "absl/strings/str_cat.h"
8#include "absl/strings/str_format.h"
9
10// Cross-platform WebSocket support using httplib
11// Skip httplib in WASM builds - use Emscripten WebSocket API instead
12#if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__)
13#ifndef _WIN32
14#define CPPHTTPLIB_OPENSSL_SUPPORT
15#endif
16#include "httplib.h"
17#endif
18
19namespace yaze {
20
21namespace net {
22
23// Native (non-WASM) implementation using httplib
24#if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__)
25
26// Platform-independent WebSocket implementation using httplib
27class WebSocketClient::Impl {
28 public:
29 Impl() : connected_(false), should_stop_(false) {}
30
31 ~Impl() { Disconnect(); }
32
33 absl::Status Connect(const std::string& host, int port) {
34 std::lock_guard<std::mutex> lock(mutex_);
35
36 if (connected_) {
37 return absl::AlreadyExistsError("Already connected");
38 }
39
40 host_ = host;
41 port_ = port;
42
43 try {
44 // httplib WebSocket connection (cross-platform)
45 std::string url = absl::StrFormat("ws://%s:%d", host, port);
46
47 // Create WebSocket connection
48 client_ = std::make_unique<httplib::Client>(host, port);
49 client_->set_connection_timeout(5, 0); // 5 seconds
50 client_->set_read_timeout(30, 0); // 30 seconds
51
52 connected_ = true;
53 should_stop_ = false;
54
55 // Start receive thread
56 receive_thread_ = std::thread([this]() { ReceiveLoop(); });
57
58 return absl::OkStatus();
59
60 } catch (const std::exception& e) {
61 return absl::UnavailableError(
62 absl::StrCat("Failed to connect: ", e.what()));
63 }
64 }
65
66 void Disconnect() {
67 std::lock_guard<std::mutex> lock(mutex_);
68
69 if (!connected_)
70 return;
71
72 should_stop_ = true;
73 connected_ = false;
74
75 if (receive_thread_.joinable()) {
76 receive_thread_.join();
77 }
78
79 client_.reset();
80 }
81
82 absl::Status Send(const std::string& message) {
83 std::lock_guard<std::mutex> lock(mutex_);
84
85 if (!connected_) {
86 return absl::FailedPreconditionError("Not connected");
87 }
88
89 try {
90 // In a real implementation, this would use WebSocket send
91 // For now, we'll use HTTP POST as fallback
92 auto res = client_->Post("/message", message, "application/json");
93
94 if (!res) {
95 return absl::UnavailableError("Failed to send message");
96 }
97
98 if (res->status != 200) {
99 return absl::InternalError(
100 absl::StrFormat("Server error: %d", res->status));
101 }
102
103 return absl::OkStatus();
104
105 } catch (const std::exception& e) {
106 return absl::InternalError(absl::StrCat("Send failed: ", e.what()));
107 }
108 }
109
110 void SetMessageCallback(std::function<void(const std::string&)> callback) {
111 std::lock_guard<std::mutex> lock(mutex_);
112 message_callback_ = callback;
113 }
114
115 void SetErrorCallback(std::function<void(const std::string&)> callback) {
116 std::lock_guard<std::mutex> lock(mutex_);
117 error_callback_ = callback;
118 }
119
120 bool IsConnected() const {
121 std::lock_guard<std::mutex> lock(mutex_);
122 return connected_;
123 }
124
125 private:
126 void ReceiveLoop() {
127 while (!should_stop_) {
128 try {
129 // Poll for messages (platform-independent)
130 std::this_thread::sleep_for(std::chrono::milliseconds(100));
131
132 // In a real WebSocket implementation, this would receive messages
133 // For now, this is a placeholder for the receive loop
134
135 } catch (const std::exception& e) {
136 if (error_callback_) {
137 error_callback_(e.what());
138 }
139 }
140 }
141 }
142
143 mutable std::mutex mutex_;
144 std::unique_ptr<httplib::Client> client_;
145 std::thread receive_thread_;
146
147 std::string host_;
148 int port_;
149 bool connected_;
150 bool should_stop_;
151
152 std::function<void(const std::string&)> message_callback_;
153 std::function<void(const std::string&)> error_callback_;
154};
155
156#elif defined(__EMSCRIPTEN__)
157
158// WASM stub - uses EmscriptenWebSocket from wasm/ directory instead
159class WebSocketClient::Impl {
160 public:
161 absl::Status Connect(const std::string&, int) {
162 return absl::UnimplementedError(
163 "Use EmscriptenWebSocket for WASM WebSocket connections");
164 }
165 void Disconnect() {}
166 absl::Status Send(const std::string&) {
167 return absl::UnimplementedError(
168 "Use EmscriptenWebSocket for WASM WebSocket connections");
169 }
170 void SetMessageCallback(std::function<void(const std::string&)>) {}
171 void SetErrorCallback(std::function<void(const std::string&)>) {}
172 bool IsConnected() const { return false; }
173};
174
175#else
176
177// Stub implementation when JSON is not available
179 public:
180 absl::Status Connect(const std::string&, int) {
181 return absl::UnimplementedError("WebSocket support requires JSON library");
182 }
183 void Disconnect() {}
184 absl::Status Send(const std::string&) {
185 return absl::UnimplementedError("WebSocket support requires JSON library");
186 }
187 void SetMessageCallback(std::function<void(const std::string&)>) {}
188 void SetErrorCallback(std::function<void(const std::string&)>) {}
189 bool IsConnected() const { return false; }
190};
191
192#endif // YAZE_WITH_JSON && !__EMSCRIPTEN__
193
194// ============================================================================
195// WebSocketClient Implementation
196// ============================================================================
197
199 : impl_(std::make_unique<Impl>()), state_(ConnectionState::kDisconnected) {}
200
204
205absl::Status WebSocketClient::Connect(const std::string& host, int port) {
206 auto status = impl_->Connect(host, port);
207
208 if (status.ok()) {
210 } else {
212 }
213
214 return status;
215}
216
222
223absl::StatusOr<SessionInfo> WebSocketClient::HostSession(
224 const std::string& session_name, const std::string& username,
225 const std::string& rom_hash, bool ai_enabled) {
226#ifdef YAZE_WITH_JSON
227 if (!IsConnected()) {
228 return absl::FailedPreconditionError("Not connected to server");
229 }
230
231 nlohmann::json message = {{"type", "host_session"},
232 {"payload",
233 {{"session_name", session_name},
234 {"username", username},
235 {"rom_hash", rom_hash},
236 {"ai_enabled", ai_enabled}}}};
237
238 auto status = SendRaw(message);
239 if (!status.ok()) {
240 return status;
241 }
242
243 // In a real implementation, we'd wait for the server response
244 // For now, return a placeholder
245 SessionInfo session;
246 session.session_name = session_name;
247 session.host = username;
248 session.rom_hash = rom_hash;
249 session.ai_enabled = ai_enabled;
250
251 current_session_ = session;
252 return session;
253#else
254 return absl::UnimplementedError("JSON support required");
255#endif
256}
257
258absl::StatusOr<SessionInfo> WebSocketClient::JoinSession(
259 const std::string& session_code, const std::string& username) {
260#ifdef YAZE_WITH_JSON
261 if (!IsConnected()) {
262 return absl::FailedPreconditionError("Not connected to server");
263 }
264
265 nlohmann::json message = {
266 {"type", "join_session"},
267 {"payload", {{"session_code", session_code}, {"username", username}}}};
268
269 auto status = SendRaw(message);
270 if (!status.ok()) {
271 return status;
272 }
273
274 // Placeholder - would wait for server response
275 SessionInfo session;
276 session.session_code = session_code;
277
278 current_session_ = session;
279 return session;
280#else
281 return absl::UnimplementedError("JSON support required");
282#endif
283}
284
286#ifdef YAZE_WITH_JSON
287 if (!InSession()) {
288 return absl::FailedPreconditionError("Not in a session");
289 }
290
291 nlohmann::json message = {{"type", "leave_session"}, {"payload", {}}};
292
293 auto status = SendRaw(message);
295 return status;
296#else
297 return absl::UnimplementedError("JSON support required");
298#endif
299}
300
301absl::Status WebSocketClient::SendChatMessage(const std::string& message,
302 const std::string& sender) {
303#ifdef YAZE_WITH_JSON
304 nlohmann::json msg = {
305 {"type", "chat_message"},
306 {"payload", {{"message", message}, {"sender", sender}}}};
307
308 return SendRaw(msg);
309#else
310 return absl::UnimplementedError("JSON support required");
311#endif
312}
313
314absl::Status WebSocketClient::SendRomSync(const std::string& diff_data,
315 const std::string& rom_hash,
316 const std::string& sender) {
317#ifdef YAZE_WITH_JSON
318 nlohmann::json message = {
319 {"type", "rom_sync"},
320 {"payload",
321 {{"diff_data", diff_data}, {"rom_hash", rom_hash}, {"sender", sender}}}};
322
323 return SendRaw(message);
324#else
325 return absl::UnimplementedError("JSON support required");
326#endif
327}
328
329absl::Status WebSocketClient::ShareProposal(const nlohmann::json& proposal_data,
330 const std::string& sender) {
331#ifdef YAZE_WITH_JSON
332 nlohmann::json message = {
333 {"type", "proposal_share"},
334 {"payload", {{"sender", sender}, {"proposal_data", proposal_data}}}};
335
336 return SendRaw(message);
337#else
338 return absl::UnimplementedError("JSON support required");
339#endif
340}
341
342absl::Status WebSocketClient::VoteOnProposal(const std::string& proposal_id,
343 bool approved,
344 const std::string& username) {
345#ifdef YAZE_WITH_JSON
346 nlohmann::json message = {{"type", "proposal_vote"},
347 {"payload",
348 {{"proposal_id", proposal_id},
349 {"approved", approved},
350 {"username", username}}}};
351
352 return SendRaw(message);
353#else
354 return absl::UnimplementedError("JSON support required");
355#endif
356}
357
359 const std::string& proposal_id, const std::string& status) {
360#ifdef YAZE_WITH_JSON
361 nlohmann::json message = {
362 {"type", "proposal_update"},
363 {"payload", {{"proposal_id", proposal_id}, {"status", status}}}};
364
365 return SendRaw(message);
366#else
367 return absl::UnimplementedError("JSON support required");
368#endif
369}
370
371void WebSocketClient::OnMessage(const std::string& type,
372 MessageCallback callback) {
373 message_callbacks_[type].push_back(callback);
374}
375
377 error_callbacks_.push_back(callback);
378}
379
381 state_callbacks_.push_back(callback);
382}
383
384absl::StatusOr<SessionInfo> WebSocketClient::GetSessionInfo() const {
385 if (!InSession()) {
386 return absl::FailedPreconditionError("Not in a session");
387 }
388 return current_session_;
389}
390
391// Private methods
392
393void WebSocketClient::HandleMessage(const std::string& message) {
394#ifdef YAZE_WITH_JSON
395 try {
396 auto json = nlohmann::json::parse(message);
397 std::string type = json["type"];
398
399 auto it = message_callbacks_.find(type);
400 if (it != message_callbacks_.end()) {
401 for (auto& callback : it->second) {
402 callback(json["payload"]);
403 }
404 }
405 } catch (const std::exception& e) {
406 HandleError(absl::StrCat("Failed to parse message: ", e.what()));
407 }
408#endif
409}
410
411void WebSocketClient::HandleError(const std::string& error) {
412 for (auto& callback : error_callbacks_) {
413 callback(error);
414 }
415}
416
418 if (state_ != state) {
419 state_ = state;
420 for (auto& callback : state_callbacks_) {
421 callback(state);
422 }
423 }
424}
425
426absl::Status WebSocketClient::SendRaw(const nlohmann::json& message) {
427#ifdef YAZE_WITH_JSON
428 try {
429 std::string msg_str = message.dump();
430 return impl_->Send(msg_str);
431 } catch (const std::exception& e) {
432 return absl::InternalError(absl::StrCat("Failed to serialize: ", e.what()));
433 }
434#else
435 return absl::UnimplementedError("JSON support required");
436#endif
437}
438
439} // namespace net
440
441} // namespace yaze
void SetErrorCallback(std::function< void(const std::string &)>)
void SetMessageCallback(std::function< void(const std::string &)>)
absl::Status Send(const std::string &)
absl::Status Connect(const std::string &, int)
void SetState(ConnectionState state)
absl::Status Connect(const std::string &host, int port=8765)
absl::StatusOr< SessionInfo > HostSession(const std::string &session_name, const std::string &username, const std::string &rom_hash, bool ai_enabled=true)
absl::Status VoteOnProposal(const std::string &proposal_id, bool approved, const std::string &username)
std::function< void(ConnectionState)> StateCallback
absl::Status UpdateProposalStatus(const std::string &proposal_id, const std::string &status)
absl::Status ShareProposal(const nlohmann::json &proposal_data, const std::string &sender)
void OnStateChange(StateCallback callback)
absl::StatusOr< SessionInfo > GetSessionInfo() const
std::function< void(const std::string &)> ErrorCallback
std::unique_ptr< Impl > impl_
void OnMessage(const std::string &type, MessageCallback callback)
absl::Status SendChatMessage(const std::string &message, const std::string &sender)
void OnError(ErrorCallback callback)
std::function< void(const nlohmann::json &)> MessageCallback
std::vector< StateCallback > state_callbacks_
std::vector< ErrorCallback > error_callbacks_
std::map< std::string, std::vector< MessageCallback > > message_callbacks_
absl::StatusOr< SessionInfo > JoinSession(const std::string &session_code, const std::string &username)
absl::Status SendRomSync(const std::string &diff_data, const std::string &rom_hash, const std::string &sender)
void HandleMessage(const std::string &message)
absl::Status SendRaw(const nlohmann::json &message)
void HandleError(const std::string &error)
ConnectionState
WebSocket connection states.
Information about the current collaboration session.