backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)

This commit is contained in:
scawful
2025-12-22 00:20:49 +00:00
parent 2934c82b75
commit 5c4cd57ff8
1259 changed files with 239160 additions and 43801 deletions

View File

@@ -8,7 +8,7 @@
#include "absl/status/status.h"
#include "app/net/rom_version_manager.h"
#include "app/net/websocket_client.h"
#include "app/rom.h"
#include "rom/rom.h"
namespace yaze {

123
src/app/net/http_client.h Normal file
View File

@@ -0,0 +1,123 @@
#ifndef YAZE_APP_NET_HTTP_CLIENT_H_
#define YAZE_APP_NET_HTTP_CLIENT_H_
#include <map>
#include <string>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
namespace yaze {
namespace net {
/**
* @brief HTTP headers type definition
*/
using Headers = std::map<std::string, std::string>;
/**
* @struct HttpResponse
* @brief HTTP response structure containing status, body, and headers
*/
struct HttpResponse {
int status_code = 0;
std::string body;
Headers headers;
bool IsSuccess() const {
return status_code >= 200 && status_code < 300;
}
bool IsClientError() const {
return status_code >= 400 && status_code < 500;
}
bool IsServerError() const {
return status_code >= 500 && status_code < 600;
}
};
/**
* @class IHttpClient
* @brief Abstract interface for HTTP client implementations
*
* This interface abstracts HTTP operations to support both native
* (using cpp-httplib) and WASM (using emscripten fetch) implementations.
* All methods return absl::Status or absl::StatusOr for consistent error handling.
*/
class IHttpClient {
public:
virtual ~IHttpClient() = default;
/**
* @brief Perform an HTTP GET request
* @param url The URL to request
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
virtual absl::StatusOr<HttpResponse> Get(
const std::string& url,
const Headers& headers = {}) = 0;
/**
* @brief Perform an HTTP POST request
* @param url The URL to post to
* @param body The request body
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
virtual absl::StatusOr<HttpResponse> Post(
const std::string& url,
const std::string& body,
const Headers& headers = {}) = 0;
/**
* @brief Perform an HTTP PUT request
* @param url The URL to put to
* @param body The request body
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
virtual absl::StatusOr<HttpResponse> Put(
const std::string& url,
const std::string& body,
const Headers& headers = {}) {
// Default implementation returns not implemented
return absl::UnimplementedError("PUT method not implemented");
}
/**
* @brief Perform an HTTP DELETE request
* @param url The URL to delete
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
virtual absl::StatusOr<HttpResponse> Delete(
const std::string& url,
const Headers& headers = {}) {
// Default implementation returns not implemented
return absl::UnimplementedError("DELETE method not implemented");
}
/**
* @brief Set a timeout for HTTP requests
* @param timeout_seconds Timeout in seconds
*/
virtual void SetTimeout(int timeout_seconds) {
timeout_seconds_ = timeout_seconds;
}
/**
* @brief Get the current timeout setting
* @return Timeout in seconds
*/
int GetTimeout() const { return timeout_seconds_; }
protected:
int timeout_seconds_ = 30; // Default 30 second timeout
};
} // namespace net
} // namespace yaze
#endif // YAZE_APP_NET_HTTP_CLIENT_H_

View File

@@ -0,0 +1,267 @@
#include "app/net/native/httplib_client.h"
#include <regex>
#include "util/macro.h" // For RETURN_IF_ERROR and ASSIGN_OR_RETURN
// Include httplib with appropriate settings
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#define CPPHTTPLIB_OPENSSL_SUPPORT
#endif
#include "httplib.h"
namespace yaze {
namespace net {
HttpLibClient::HttpLibClient() {
// Constructor
}
HttpLibClient::~HttpLibClient() {
// Cleanup cached clients
client_cache_.clear();
}
absl::Status HttpLibClient::ParseUrl(const std::string& url,
std::string& scheme,
std::string& host,
int& port,
std::string& path) const {
// Basic URL regex pattern
std::regex url_regex(R"(^(https?):\/\/([^:\/\s]+)(?::(\d+))?(\/.*)?$)");
std::smatch matches;
if (!std::regex_match(url, matches, url_regex)) {
return absl::InvalidArgumentError("Invalid URL format: " + url);
}
scheme = matches[1].str();
host = matches[2].str();
// Parse port or use defaults
if (matches[3].matched) {
port = std::stoi(matches[3].str());
} else {
port = (scheme == "https") ? 443 : 80;
}
// Parse path (default to "/" if empty)
path = matches[4].matched ? matches[4].str() : "/";
return absl::OkStatus();
}
absl::StatusOr<std::shared_ptr<httplib::Client>> HttpLibClient::GetOrCreateClient(
const std::string& scheme,
const std::string& host,
int port) {
// Create cache key
std::string cache_key = scheme + "://" + host + ":" + std::to_string(port);
// Check if client exists in cache
auto it = client_cache_.find(cache_key);
if (it != client_cache_.end() && it->second) {
return it->second;
}
// Create new client
std::shared_ptr<httplib::Client> client;
if (scheme == "https") {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
client = std::make_shared<httplib::Client>(host, port);
client->enable_server_certificate_verification(false); // For development
#else
return absl::UnimplementedError(
"HTTPS not supported: OpenSSL support not compiled in");
#endif
} else if (scheme == "http") {
client = std::make_shared<httplib::Client>(host, port);
} else {
return absl::InvalidArgumentError("Unsupported URL scheme: " + scheme);
}
if (!client) {
return absl::InternalError("Failed to create HTTP client");
}
// Set timeout from base class
client->set_connection_timeout(timeout_seconds_);
client->set_read_timeout(timeout_seconds_);
client->set_write_timeout(timeout_seconds_);
// Cache the client
client_cache_[cache_key] = client;
return client;
}
Headers HttpLibClient::ConvertHeaders(const void* httplib_headers) const {
Headers result;
if (httplib_headers) {
const auto& headers = *static_cast<const httplib::Headers*>(httplib_headers);
for (const auto& header : headers) {
result[header.first] = header.second;
}
}
return result;
}
absl::StatusOr<HttpResponse> HttpLibClient::Get(
const std::string& url,
const Headers& headers) {
std::string scheme, host, path;
int port;
RETURN_IF_ERROR(ParseUrl(url, scheme, host, port, path));
auto client_or = GetOrCreateClient(scheme, host, port);
ASSIGN_OR_RETURN(auto client, client_or);
// Convert headers to httplib format
httplib::Headers httplib_headers;
for (const auto& [key, value] : headers) {
httplib_headers.emplace(key, value);
}
// Perform GET request
auto res = client->Get(path.c_str(), httplib_headers);
if (!res) {
return absl::UnavailableError("HTTP GET request failed: " + url);
}
HttpResponse response;
response.status_code = res->status;
response.body = res->body;
response.headers = ConvertHeaders(&res->headers);
return response;
}
absl::StatusOr<HttpResponse> HttpLibClient::Post(
const std::string& url,
const std::string& body,
const Headers& headers) {
std::string scheme, host, path;
int port;
RETURN_IF_ERROR(ParseUrl(url, scheme, host, port, path));
auto client_or = GetOrCreateClient(scheme, host, port);
ASSIGN_OR_RETURN(auto client, client_or);
// Convert headers to httplib format
httplib::Headers httplib_headers;
for (const auto& [key, value] : headers) {
httplib_headers.emplace(key, value);
}
// Set Content-Type if not provided
if (httplib_headers.find("Content-Type") == httplib_headers.end()) {
httplib_headers.emplace("Content-Type", "application/json");
}
// Perform POST request
auto res = client->Post(path.c_str(), httplib_headers, body,
httplib_headers.find("Content-Type")->second);
if (!res) {
return absl::UnavailableError("HTTP POST request failed: " + url);
}
HttpResponse response;
response.status_code = res->status;
response.body = res->body;
response.headers = ConvertHeaders(&res->headers);
return response;
}
absl::StatusOr<HttpResponse> HttpLibClient::Put(
const std::string& url,
const std::string& body,
const Headers& headers) {
std::string scheme, host, path;
int port;
RETURN_IF_ERROR(ParseUrl(url, scheme, host, port, path));
auto client_or = GetOrCreateClient(scheme, host, port);
ASSIGN_OR_RETURN(auto client, client_or);
// Convert headers to httplib format
httplib::Headers httplib_headers;
for (const auto& [key, value] : headers) {
httplib_headers.emplace(key, value);
}
// Set Content-Type if not provided
if (httplib_headers.find("Content-Type") == httplib_headers.end()) {
httplib_headers.emplace("Content-Type", "application/json");
}
// Perform PUT request
auto res = client->Put(path.c_str(), httplib_headers, body,
httplib_headers.find("Content-Type")->second);
if (!res) {
return absl::UnavailableError("HTTP PUT request failed: " + url);
}
HttpResponse response;
response.status_code = res->status;
response.body = res->body;
response.headers = ConvertHeaders(&res->headers);
return response;
}
absl::StatusOr<HttpResponse> HttpLibClient::Delete(
const std::string& url,
const Headers& headers) {
std::string scheme, host, path;
int port;
RETURN_IF_ERROR(ParseUrl(url, scheme, host, port, path));
auto client_or = GetOrCreateClient(scheme, host, port);
ASSIGN_OR_RETURN(auto client, client_or);
// Convert headers to httplib format
httplib::Headers httplib_headers;
for (const auto& [key, value] : headers) {
httplib_headers.emplace(key, value);
}
// Perform DELETE request
auto res = client->Delete(path.c_str(), httplib_headers);
if (!res) {
return absl::UnavailableError("HTTP DELETE request failed: " + url);
}
HttpResponse response;
response.status_code = res->status;
response.body = res->body;
response.headers = ConvertHeaders(&res->headers);
return response;
}
void HttpLibClient::SetTimeout(int timeout_seconds) {
IHttpClient::SetTimeout(timeout_seconds);
// Clear client cache to force recreation with new timeout
client_cache_.clear();
}
} // namespace net
} // namespace yaze

View File

@@ -0,0 +1,121 @@
#ifndef YAZE_APP_NET_NATIVE_HTTPLIB_CLIENT_H_
#define YAZE_APP_NET_NATIVE_HTTPLIB_CLIENT_H_
#include <memory>
#include <string>
#include "app/net/http_client.h"
// Forward declaration to avoid including httplib.h in header
namespace httplib {
class Client;
}
namespace yaze {
namespace net {
/**
* @class HttpLibClient
* @brief Native HTTP client implementation using cpp-httplib
*
* This implementation wraps the cpp-httplib library for native builds,
* providing HTTP/HTTPS support with optional SSL/TLS via OpenSSL.
*/
class HttpLibClient : public IHttpClient {
public:
HttpLibClient();
~HttpLibClient() override;
/**
* @brief Perform an HTTP GET request
* @param url The URL to request
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> Get(
const std::string& url,
const Headers& headers = {}) override;
/**
* @brief Perform an HTTP POST request
* @param url The URL to post to
* @param body The request body
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> Post(
const std::string& url,
const std::string& body,
const Headers& headers = {}) override;
/**
* @brief Perform an HTTP PUT request
* @param url The URL to put to
* @param body The request body
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> Put(
const std::string& url,
const std::string& body,
const Headers& headers = {}) override;
/**
* @brief Perform an HTTP DELETE request
* @param url The URL to delete
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> Delete(
const std::string& url,
const Headers& headers = {}) override;
/**
* @brief Set a timeout for HTTP requests
* @param timeout_seconds Timeout in seconds
*/
void SetTimeout(int timeout_seconds) override;
private:
/**
* @brief Parse URL into components
* @param url The URL to parse
* @param scheme Output: URL scheme (http/https)
* @param host Output: Host name
* @param port Output: Port number
* @param path Output: Path component
* @return Status indicating success or failure
*/
absl::Status ParseUrl(const std::string& url,
std::string& scheme,
std::string& host,
int& port,
std::string& path) const;
/**
* @brief Create or get cached httplib client for a host
* @param scheme URL scheme (http/https)
* @param host Host name
* @param port Port number
* @return httplib::Client pointer or error
*/
absl::StatusOr<std::shared_ptr<httplib::Client>> GetOrCreateClient(
const std::string& scheme,
const std::string& host,
int port);
/**
* @brief Convert httplib headers to our Headers type
* @param httplib_headers httplib header structure
* @return Headers map
*/
Headers ConvertHeaders(const void* httplib_headers) const;
// Cache clients per host to avoid reconnection overhead
std::map<std::string, std::shared_ptr<httplib::Client>> client_cache_;
};
} // namespace net
} // namespace yaze
#endif // YAZE_APP_NET_NATIVE_HTTPLIB_CLIENT_H_

View File

@@ -0,0 +1,273 @@
#include "app/net/native/httplib_websocket.h"
#include <chrono>
#include <regex>
#include "util/macro.h" // For RETURN_IF_ERROR
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
#define CPPHTTPLIB_OPENSSL_SUPPORT
#endif
#include "httplib.h"
namespace yaze {
namespace net {
HttpLibWebSocket::HttpLibWebSocket() : stop_receive_(false) {
state_ = WebSocketState::kDisconnected;
}
HttpLibWebSocket::~HttpLibWebSocket() {
if (state_ != WebSocketState::kDisconnected) {
Close();
}
}
absl::Status HttpLibWebSocket::ParseWebSocketUrl(const std::string& ws_url,
std::string& http_url) {
// Convert ws:// to http:// and wss:// to https://
std::regex ws_regex(R"(^(wss?)://(.+)$)");
std::smatch matches;
if (!std::regex_match(ws_url, matches, ws_regex)) {
return absl::InvalidArgumentError("Invalid WebSocket URL: " + ws_url);
}
std::string scheme = matches[1].str();
std::string rest = matches[2].str();
if (scheme == "ws") {
http_url = "http://" + rest;
} else if (scheme == "wss") {
http_url = "https://" + rest;
} else {
return absl::InvalidArgumentError("Invalid WebSocket scheme: " + scheme);
}
url_ = ws_url;
return absl::OkStatus();
}
absl::Status HttpLibWebSocket::Connect(const std::string& url) {
if (state_ != WebSocketState::kDisconnected) {
return absl::FailedPreconditionError(
"WebSocket already connected or connecting");
}
state_ = WebSocketState::kConnecting;
// Convert WebSocket URL to HTTP URL
RETURN_IF_ERROR(ParseWebSocketUrl(url, http_endpoint_));
// Parse HTTP URL to extract host and port
std::regex url_regex(R"(^(https?)://([^:/\s]+)(?::(\d+))?(/.*)?)$)");
std::smatch matches;
if (!std::regex_match(http_endpoint_, matches, url_regex)) {
state_ = WebSocketState::kError;
return absl::InvalidArgumentError("Invalid HTTP URL: " + http_endpoint_);
}
std::string scheme = matches[1].str();
std::string host = matches[2].str();
int port = matches[3].matched ? std::stoi(matches[3].str())
: (scheme == "https" ? 443 : 80);
// Create HTTP client
if (scheme == "https") {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
client_ = std::make_shared<httplib::Client>(host, port);
client_->enable_server_certificate_verification(false); // For development
#else
state_ = WebSocketState::kError;
return absl::UnimplementedError(
"WSS not supported: OpenSSL support not compiled in");
#endif
} else {
client_ = std::make_shared<httplib::Client>(host, port);
}
if (!client_) {
state_ = WebSocketState::kError;
return absl::InternalError("Failed to create HTTP client");
}
// Set reasonable timeouts
client_->set_connection_timeout(10);
client_->set_read_timeout(30);
// Note: This is a simplified implementation. A real WebSocket implementation
// would perform the WebSocket handshake here. For now, we'll use HTTP
// long-polling as a fallback.
// Generate session ID for this connection
session_id_ = "session_" + std::to_string(
std::chrono::system_clock::now().time_since_epoch().count());
state_ = WebSocketState::kConnected;
// Call open callback if set
if (open_callback_) {
open_callback_();
}
// Start receive loop in background thread
stop_receive_ = false;
receive_thread_ = std::thread([this]() { ReceiveLoop(); });
return absl::OkStatus();
}
absl::Status HttpLibWebSocket::Send(const std::string& message) {
if (state_ != WebSocketState::kConnected) {
return absl::FailedPreconditionError("WebSocket not connected");
}
if (!client_) {
return absl::InternalError("HTTP client not initialized");
}
// Note: This is a simplified implementation using HTTP POST
// A real WebSocket would send frames over the persistent connection
httplib::Headers headers = {
{"Content-Type", "text/plain"},
{"X-Session-Id", session_id_}
};
auto res = client_->Post("/send", headers, message, "text/plain");
if (!res) {
return absl::UnavailableError("Failed to send message");
}
if (res->status != 200) {
return absl::InternalError("Server returned status " +
std::to_string(res->status));
}
return absl::OkStatus();
}
absl::Status HttpLibWebSocket::SendBinary(const uint8_t* data, size_t length) {
if (state_ != WebSocketState::kConnected) {
return absl::FailedPreconditionError("WebSocket not connected");
}
if (!client_) {
return absl::InternalError("HTTP client not initialized");
}
// Convert binary data to string for HTTP transport
std::string body(reinterpret_cast<const char*>(data), length);
httplib::Headers headers = {
{"Content-Type", "application/octet-stream"},
{"X-Session-Id", session_id_}
};
auto res = client_->Post("/send-binary", headers, body,
"application/octet-stream");
if (!res) {
return absl::UnavailableError("Failed to send binary data");
}
if (res->status != 200) {
return absl::InternalError("Server returned status " +
std::to_string(res->status));
}
return absl::OkStatus();
}
absl::Status HttpLibWebSocket::Close(int code, const std::string& reason) {
if (state_ == WebSocketState::kDisconnected ||
state_ == WebSocketState::kClosed) {
return absl::OkStatus();
}
state_ = WebSocketState::kClosing;
// Stop receive loop
StopReceiveLoop();
if (client_) {
// Send close notification to server
httplib::Headers headers = {
{"X-Session-Id", session_id_},
{"X-Close-Code", std::to_string(code)},
{"X-Close-Reason", reason}
};
client_->Post("/close", headers, "", "text/plain");
client_.reset();
}
state_ = WebSocketState::kClosed;
// Call close callback if set
if (close_callback_) {
close_callback_(code, reason);
}
state_ = WebSocketState::kDisconnected;
return absl::OkStatus();
}
void HttpLibWebSocket::ReceiveLoop() {
while (!stop_receive_ && state_ == WebSocketState::kConnected) {
if (!client_) {
break;
}
// Long-polling: make a request that blocks until there's a message
httplib::Headers headers = {
{"X-Session-Id", session_id_}
};
auto res = client_->Get("/poll", headers);
if (stop_receive_) {
break;
}
if (!res) {
// Connection error
if (error_callback_) {
error_callback_("Connection lost");
}
break;
}
if (res->status == 200 && !res->body.empty()) {
// Received a message
if (message_callback_) {
message_callback_(res->body);
}
} else if (res->status == 204) {
// No content - continue polling
continue;
} else if (res->status >= 400) {
// Error from server
if (error_callback_) {
error_callback_("Server error: " + std::to_string(res->status));
}
break;
}
// Small delay to prevent tight loop
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
void HttpLibWebSocket::StopReceiveLoop() {
stop_receive_ = true;
if (receive_thread_.joinable()) {
receive_thread_.join();
}
}
} // namespace net
} // namespace yaze

View File

@@ -0,0 +1,144 @@
#ifndef YAZE_APP_NET_NATIVE_HTTPLIB_WEBSOCKET_H_
#define YAZE_APP_NET_NATIVE_HTTPLIB_WEBSOCKET_H_
#include <atomic>
#include <memory>
#include <thread>
#include "app/net/websocket_interface.h"
// Forward declaration
namespace httplib {
class Client;
}
namespace yaze {
namespace net {
/**
* @class HttpLibWebSocket
* @brief Native WebSocket implementation using HTTP fallback
*
* Note: cpp-httplib doesn't have full WebSocket support, so this
* implementation uses HTTP long-polling as a fallback. For production
* use, consider integrating a proper WebSocket library like websocketpp
* or libwebsockets.
*/
class HttpLibWebSocket : public IWebSocket {
public:
HttpLibWebSocket();
~HttpLibWebSocket() override;
/**
* @brief Connect to a WebSocket server
* @param url The WebSocket URL (ws:// or wss://)
* @return Status indicating success or failure
*/
absl::Status Connect(const std::string& url) override;
/**
* @brief Send a text message
* @param message The text message to send
* @return Status indicating success or failure
*/
absl::Status Send(const std::string& message) override;
/**
* @brief Send a binary message
* @param data The binary data to send
* @param length The length of the data
* @return Status indicating success or failure
*/
absl::Status SendBinary(const uint8_t* data, size_t length) override;
/**
* @brief Close the WebSocket connection
* @param code Optional close code
* @param reason Optional close reason
* @return Status indicating success or failure
*/
absl::Status Close(int code = 1000,
const std::string& reason = "") override;
/**
* @brief Get the current connection state
* @return Current WebSocket state
*/
WebSocketState GetState() const override { return state_; }
/**
* @brief Set callback for text message events
* @param callback Function to call when a text message is received
*/
void OnMessage(MessageCallback callback) override {
message_callback_ = callback;
}
/**
* @brief Set callback for binary message events
* @param callback Function to call when binary data is received
*/
void OnBinaryMessage(BinaryMessageCallback callback) override {
binary_message_callback_ = callback;
}
/**
* @brief Set callback for connection open events
* @param callback Function to call when connection is established
*/
void OnOpen(OpenCallback callback) override {
open_callback_ = callback;
}
/**
* @brief Set callback for connection close events
* @param callback Function to call when connection is closed
*/
void OnClose(CloseCallback callback) override {
close_callback_ = callback;
}
/**
* @brief Set callback for error events
* @param callback Function to call when an error occurs
*/
void OnError(ErrorCallback callback) override {
error_callback_ = callback;
}
private:
/**
* @brief Parse WebSocket URL into HTTP components
* @param ws_url WebSocket URL (ws:// or wss://)
* @param http_url Output: Converted HTTP URL
* @return Status indicating success or failure
*/
absl::Status ParseWebSocketUrl(const std::string& ws_url,
std::string& http_url);
/**
* @brief Background thread for receiving messages (polling)
*/
void ReceiveLoop();
/**
* @brief Stop the receive loop
*/
void StopReceiveLoop();
// HTTP client for fallback implementation
std::shared_ptr<httplib::Client> client_;
// Background receive thread
std::thread receive_thread_;
std::atomic<bool> stop_receive_;
// Connection details
std::string session_id_;
std::string http_endpoint_;
};
} // namespace net
} // namespace yaze
#endif // YAZE_APP_NET_NATIVE_HTTPLIB_WEBSOCKET_H_

View File

@@ -2,6 +2,8 @@
# Yaze Net Library
# ==============================================================================
# This library contains networking and collaboration functionality:
# - Network abstraction layer (HTTP/WebSocket interfaces)
# - Platform-specific implementations (native/WASM)
# - ROM version management
# - Proposal approval system
# - Collaboration utilities
@@ -9,13 +11,37 @@
# Dependencies: yaze_util, absl
# ==============================================================================
# Base network sources (always included)
set(
YAZE_NET_SRC
YAZE_NET_BASE_SRC
app/net/rom_version_manager.cc
app/net/websocket_client.cc
app/net/collaboration_service.cc
app/net/network_factory.cc
)
# Platform-specific network implementation
if(EMSCRIPTEN)
# WASM/Emscripten implementation
set(
YAZE_NET_PLATFORM_SRC
app/net/wasm/emscripten_http_client.cc
app/net/wasm/emscripten_websocket.cc
)
message(STATUS " - Using Emscripten network implementation (Fetch API & WebSocket)")
else()
# Native implementation
set(
YAZE_NET_PLATFORM_SRC
app/net/native/httplib_client.cc
app/net/native/httplib_websocket.cc
)
message(STATUS " - Using native network implementation (cpp-httplib)")
endif()
# Combine all sources
set(YAZE_NET_SRC ${YAZE_NET_BASE_SRC} ${YAZE_NET_PLATFORM_SRC})
if(YAZE_WITH_GRPC)
# Add ROM service implementation (disabled - proto field mismatch)
# list(APPEND YAZE_NET_SRC app/net/rom_service_impl.cc)
@@ -43,6 +69,20 @@ target_link_libraries(yaze_net PUBLIC
${YAZE_SDL2_TARGETS}
)
# Add Emscripten-specific flags for WASM builds
if(EMSCRIPTEN)
# Enable Fetch API for HTTP requests
target_compile_options(yaze_net PUBLIC "-sFETCH=1")
target_link_options(yaze_net PUBLIC "-sFETCH=1")
# WebSocket support requires linking websocket.js library
# The <emscripten/websocket.h> header provides the API, but the
# implementation is in the websocket.js library
target_link_libraries(yaze_net PUBLIC websocket.js)
message(STATUS " - Emscripten Fetch API and WebSocket enabled")
endif()
# Add JSON and httplib support if enabled
if(YAZE_WITH_JSON)
# Link nlohmann_json which provides the include directories automatically
@@ -90,12 +130,6 @@ if(YAZE_WITH_JSON)
endif()
endif()
# Add gRPC support for ROM service
if(YAZE_WITH_GRPC)
target_link_libraries(yaze_net PUBLIC yaze_grpc_support)
message(STATUS " - gRPC ROM service enabled")
endif()
set_target_properties(yaze_net PROPERTIES
POSITION_INDEPENDENT_CODE ON
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"

View File

@@ -0,0 +1,53 @@
#include "app/net/network_factory.h"
#ifdef __EMSCRIPTEN__
#include "app/net/wasm/emscripten_http_client.h"
#include "app/net/wasm/emscripten_websocket.h"
#else
#include "app/net/native/httplib_client.h"
#include "app/net/native/httplib_websocket.h"
#endif
namespace yaze {
namespace net {
std::unique_ptr<IHttpClient> CreateHttpClient() {
#ifdef __EMSCRIPTEN__
return std::make_unique<EmscriptenHttpClient>();
#else
return std::make_unique<HttpLibClient>();
#endif
}
std::unique_ptr<IWebSocket> CreateWebSocket() {
#ifdef __EMSCRIPTEN__
return std::make_unique<EmscriptenWebSocket>();
#else
return std::make_unique<HttpLibWebSocket>();
#endif
}
bool IsSSLSupported() {
#ifdef __EMSCRIPTEN__
// WASM in browser always supports SSL/TLS through browser APIs
return true;
#else
// Native builds depend on OpenSSL availability
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
return true;
#else
return false;
#endif
#endif
}
std::string GetNetworkPlatform() {
#ifdef __EMSCRIPTEN__
return "wasm";
#else
return "native";
#endif
}
} // namespace net
} // namespace yaze

View File

@@ -0,0 +1,56 @@
#ifndef YAZE_APP_NET_NETWORK_FACTORY_H_
#define YAZE_APP_NET_NETWORK_FACTORY_H_
#include <memory>
#include <string>
#include "app/net/http_client.h"
#include "app/net/websocket_interface.h"
namespace yaze {
namespace net {
/**
* @brief Factory functions for creating network clients
*
* These functions return the appropriate implementation based on the
* build platform (native or WASM). This allows the rest of the codebase
* to use networking features without worrying about platform differences.
*/
/**
* @brief Create an HTTP client for the current platform
* @return Unique pointer to IHttpClient implementation
*
* Returns:
* - HttpLibClient for native builds
* - EmscriptenHttpClient for WASM builds
*/
std::unique_ptr<IHttpClient> CreateHttpClient();
/**
* @brief Create a WebSocket client for the current platform
* @return Unique pointer to IWebSocket implementation
*
* Returns:
* - HttpLibWebSocket (or native WebSocket) for native builds
* - EmscriptenWebSocket for WASM builds
*/
std::unique_ptr<IWebSocket> CreateWebSocket();
/**
* @brief Check if the current platform supports SSL/TLS
* @return true if SSL/TLS is available, false otherwise
*/
bool IsSSLSupported();
/**
* @brief Get the platform name for debugging
* @return Platform string ("native", "wasm", etc.)
*/
std::string GetNetworkPlatform();
} // namespace net
} // namespace yaze
#endif // YAZE_APP_NET_NETWORK_FACTORY_H_

View File

@@ -1,186 +0,0 @@
#include "app/net/rom_service_impl.h"
#ifdef YAZE_WITH_GRPC
#include "absl/strings/str_format.h"
#include "app/net/rom_version_manager.h"
#include "app/rom.h"
// Proto namespace alias for convenience
namespace rom_svc = ::yaze::proto;
namespace yaze {
namespace net {
RomServiceImpl::RomServiceImpl(Rom* rom, RomVersionManager* version_manager,
ProposalApprovalManager* approval_manager)
: rom_(rom),
version_mgr_(version_manager),
approval_mgr_(approval_manager) {}
void RomServiceImpl::SetConfig(const Config& config) {
config_ = config;
}
grpc::Status RomServiceImpl::ReadBytes(grpc::ServerContext* context,
const rom_svc::ReadBytesRequest* request,
rom_svc::ReadBytesResponse* response) {
if (!rom_ || !rom_->is_loaded()) {
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
"ROM not loaded");
}
uint32_t address = request->address();
uint32_t length = request->length();
// Validate range
if (address + length > rom_->size()) {
return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
absl::StrFormat("Read beyond ROM: 0x%X+%d > %d",
address, length, rom_->size()));
}
// Read data
const auto* data = rom_->data() + address;
response->set_data(data, length);
response->set_success(true);
return grpc::Status::OK;
}
grpc::Status RomServiceImpl::WriteBytes(
grpc::ServerContext* context, const rom_svc::WriteBytesRequest* request,
rom_svc::WriteBytesResponse* response) {
if (!rom_ || !rom_->is_loaded()) {
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
"ROM not loaded");
}
uint32_t address = request->address();
const std::string& data = request->data();
// Validate range
if (address + data.size() > rom_->size()) {
return grpc::Status(grpc::StatusCode::OUT_OF_RANGE,
absl::StrFormat("Write beyond ROM: 0x%X+%zu > %d",
address, data.size(), rom_->size()));
}
// Check if approval required
if (config_.require_approval_for_writes && approval_mgr_) {
// Create a proposal for this write
std::string proposal_id =
absl::StrFormat("write_0x%X_%zu_bytes", address, data.size());
if (request->has_proposal_id()) {
proposal_id = request->proposal_id();
}
// Check if proposal is approved
auto status = approval_mgr_->GetProposalStatus(proposal_id);
if (status != ProposalApprovalManager::ApprovalStatus::kApproved) {
response->set_success(false);
response->set_message("Write requires approval");
response->set_proposal_id(proposal_id);
return grpc::Status::OK; // Not an error, just needs approval
}
}
// Create snapshot before write
if (version_mgr_) {
std::string snapshot_desc = absl::StrFormat(
"Before write to 0x%X (%zu bytes)", address, data.size());
auto snapshot_result = version_mgr_->CreateSnapshot(snapshot_desc);
if (snapshot_result.ok()) {
response->set_snapshot_id(std::to_string(snapshot_result.value()));
}
}
// Perform write
std::memcpy(rom_->mutable_data() + address, data.data(), data.size());
response->set_success(true);
response->set_message("Write successful");
return grpc::Status::OK;
}
grpc::Status RomServiceImpl::GetRomInfo(
grpc::ServerContext* context, const rom_svc::GetRomInfoRequest* request,
rom_svc::GetRomInfoResponse* response) {
if (!rom_ || !rom_->is_loaded()) {
return grpc::Status(grpc::StatusCode::FAILED_PRECONDITION,
"ROM not loaded");
}
auto* info = response->mutable_info();
info->set_title(rom_->title());
info->set_size(rom_->size());
info->set_is_loaded(rom_->is_loaded());
info->set_filename(rom_->filename());
return grpc::Status::OK;
}
grpc::Status RomServiceImpl::GetTileData(
grpc::ServerContext* context, const rom_svc::GetTileDataRequest* request,
rom_svc::GetTileDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"GetTileData not yet implemented");
}
grpc::Status RomServiceImpl::SetTileData(
grpc::ServerContext* context, const rom_svc::SetTileDataRequest* request,
rom_svc::SetTileDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"SetTileData not yet implemented");
}
grpc::Status RomServiceImpl::GetMapData(
grpc::ServerContext* context, const rom_svc::GetMapDataRequest* request,
rom_svc::GetMapDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"GetMapData not yet implemented");
}
grpc::Status RomServiceImpl::SetMapData(
grpc::ServerContext* context, const rom_svc::SetMapDataRequest* request,
rom_svc::SetMapDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"SetMapData not yet implemented");
}
grpc::Status RomServiceImpl::GetSpriteData(
grpc::ServerContext* context, const rom_svc::GetSpriteDataRequest* request,
rom_svc::GetSpriteDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"GetSpriteData not yet implemented");
}
grpc::Status RomServiceImpl::SetSpriteData(
grpc::ServerContext* context, const rom_svc::SetSpriteDataRequest* request,
rom_svc::SetSpriteDataResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"SetSpriteData not yet implemented");
}
grpc::Status RomServiceImpl::GetDialogue(
grpc::ServerContext* context, const rom_svc::GetDialogueRequest* request,
rom_svc::GetDialogueResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"GetDialogue not yet implemented");
}
grpc::Status RomServiceImpl::SetDialogue(
grpc::ServerContext* context, const rom_svc::SetDialogueRequest* request,
rom_svc::SetDialogueResponse* response) {
return grpc::Status(grpc::StatusCode::UNIMPLEMENTED,
"SetDialogue not yet implemented");
}
} // namespace net
} // namespace yaze
#endif // YAZE_WITH_GRPC

View File

@@ -1,175 +0,0 @@
#ifndef YAZE_APP_NET_ROM_SERVICE_IMPL_H_
#define YAZE_APP_NET_ROM_SERVICE_IMPL_H_
#include <memory>
#include <string>
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#ifdef YAZE_WITH_GRPC
#ifdef _WIN32
#pragma push_macro("DWORD")
#pragma push_macro("ERROR")
#undef DWORD
#undef ERROR
#endif // _WIN32
#include <grpcpp/grpcpp.h>
#include "protos/rom_service.grpc.pb.h"
#ifdef _WIN32
#pragma pop_macro("DWORD")
#pragma pop_macro("ERROR")
#endif // _WIN32
// Note: Proto files will be generated to build directory
#endif
#include "app/net/rom_version_manager.h"
#include "app/rom.h"
namespace yaze {
namespace net {
#ifdef YAZE_WITH_GRPC
/**
* @brief gRPC service implementation for remote ROM manipulation
*
* Enables remote clients (like z3ed CLI) to:
* - Read/write ROM data
* - Submit proposals for collaborative editing
* - Manage ROM versions and snapshots
* - Query ROM structures (overworld, dungeons, sprites)
*
* Thread-safe and designed for concurrent access.
*/
class RomServiceImpl final : public proto::RomService::Service {
public:
/**
* @brief Configuration for the ROM service
*/
struct Config {
bool require_approval_for_writes = true; // Submit writes as proposals
bool enable_version_management = true; // Auto-snapshot before changes
int max_read_size_bytes = 1024 * 1024; // 1MB max per read
bool allow_raw_rom_access = true; // Allow direct byte access
};
/**
* @brief Construct ROM service
* @param rom Pointer to ROM instance (not owned)
* @param version_mgr Pointer to version manager (not owned, optional)
* @param approval_mgr Pointer to approval manager (not owned, optional)
*/
RomServiceImpl(Rom* rom, RomVersionManager* version_mgr = nullptr,
ProposalApprovalManager* approval_mgr = nullptr);
~RomServiceImpl() override = default;
// Initialize with configuration
void SetConfig(const Config& config);
// =========================================================================
// Basic ROM Operations
// =========================================================================
grpc::Status ReadBytes(grpc::ServerContext* context,
const proto::ReadBytesRequest* request,
proto::ReadBytesResponse* response) override;
grpc::Status WriteBytes(grpc::ServerContext* context,
const proto::WriteBytesRequest* request,
proto::WriteBytesResponse* response) override;
grpc::Status GetRomInfo(grpc::ServerContext* context,
const proto::GetRomInfoRequest* request,
proto::GetRomInfoResponse* response) override;
// =========================================================================
// Overworld Operations
// =========================================================================
grpc::Status ReadOverworldMap(
grpc::ServerContext* context,
const proto::ReadOverworldMapRequest* request,
proto::ReadOverworldMapResponse* response) override;
grpc::Status WriteOverworldTile(
grpc::ServerContext* context,
const proto::WriteOverworldTileRequest* request,
proto::WriteOverworldTileResponse* response) override;
// =========================================================================
// Dungeon Operations
// =========================================================================
grpc::Status ReadDungeonRoom(
grpc::ServerContext* context,
const proto::ReadDungeonRoomRequest* request,
proto::ReadDungeonRoomResponse* response) override;
grpc::Status WriteDungeonTile(
grpc::ServerContext* context,
const proto::WriteDungeonTileRequest* request,
proto::WriteDungeonTileResponse* response) override;
// =========================================================================
// Sprite Operations
// =========================================================================
grpc::Status ReadSprite(grpc::ServerContext* context,
const proto::ReadSpriteRequest* request,
proto::ReadSpriteResponse* response) override;
// =========================================================================
// Proposal System
// =========================================================================
grpc::Status SubmitRomProposal(
grpc::ServerContext* context,
const proto::SubmitRomProposalRequest* request,
proto::SubmitRomProposalResponse* response) override;
grpc::Status GetProposalStatus(
grpc::ServerContext* context,
const proto::GetProposalStatusRequest* request,
proto::GetProposalStatusResponse* response) override;
// =========================================================================
// Version Management
// =========================================================================
grpc::Status CreateSnapshot(grpc::ServerContext* context,
const proto::CreateSnapshotRequest* request,
proto::CreateSnapshotResponse* response) override;
grpc::Status RestoreSnapshot(
grpc::ServerContext* context,
const proto::RestoreSnapshotRequest* request,
proto::RestoreSnapshotResponse* response) override;
grpc::Status ListSnapshots(grpc::ServerContext* context,
const proto::ListSnapshotsRequest* request,
proto::ListSnapshotsResponse* response) override;
private:
Config config_;
Rom* rom_; // Not owned
RomVersionManager* version_mgr_; // Not owned, may be null
ProposalApprovalManager* approval_mgr_; // Not owned, may be null
// Helper to check if ROM is loaded
grpc::Status ValidateRomLoaded();
// Helper to create snapshot before write operations
absl::Status MaybeCreateSnapshot(const std::string& description);
};
#endif // YAZE_WITH_GRPC
} // namespace net
} // namespace yaze
#endif // YAZE_APP_NET_ROM_SERVICE_IMPL_H_

View File

@@ -8,7 +8,7 @@
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "app/rom.h"
#include "rom/rom.h"
#ifdef YAZE_WITH_JSON
#include "nlohmann/json.hpp"

View File

@@ -0,0 +1,195 @@
#ifdef __EMSCRIPTEN__
#include "app/net/wasm/emscripten_http_client.h"
#include <emscripten/fetch.h>
#include <cstring>
#include <vector>
namespace yaze {
namespace net {
EmscriptenHttpClient::EmscriptenHttpClient() {
// Constructor
}
EmscriptenHttpClient::~EmscriptenHttpClient() {
// Destructor
}
void EmscriptenHttpClient::OnFetchSuccess(emscripten_fetch_t* fetch) {
FetchResult* result = static_cast<FetchResult*>(fetch->userData);
{
std::lock_guard<std::mutex> lock(result->mutex);
result->success = true;
result->status_code = fetch->status;
result->body = std::string(fetch->data, fetch->numBytes);
// Parse response headers if available
// Note: Emscripten fetch API has limited header access due to CORS
// Only headers exposed by Access-Control-Expose-Headers are available
result->completed = true;
}
result->cv.notify_one();
emscripten_fetch_close(fetch);
}
void EmscriptenHttpClient::OnFetchError(emscripten_fetch_t* fetch) {
FetchResult* result = static_cast<FetchResult*>(fetch->userData);
{
std::lock_guard<std::mutex> lock(result->mutex);
result->success = false;
result->status_code = fetch->status;
if (fetch->status == 0) {
result->error_message = "Network error or CORS blocking";
} else {
result->error_message = "HTTP error: " + std::to_string(fetch->status);
}
result->completed = true;
}
result->cv.notify_one();
emscripten_fetch_close(fetch);
}
void EmscriptenHttpClient::OnFetchProgress(emscripten_fetch_t* fetch) {
// Progress callback - can be used for download progress
// Not implemented for now
(void)fetch; // Suppress unused parameter warning
}
absl::StatusOr<HttpResponse> EmscriptenHttpClient::PerformFetch(
const std::string& method,
const std::string& url,
const std::string& body,
const Headers& headers) {
// Create result structure
FetchResult result;
// Initialize fetch attributes
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
// Set HTTP method
strncpy(attr.requestMethod, method.c_str(), sizeof(attr.requestMethod) - 1);
attr.requestMethod[sizeof(attr.requestMethod) - 1] = '\0';
// Set attributes for synchronous-style operation
// We use async fetch with callbacks but wait for completion
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
// Set callbacks
attr.onsuccess = OnFetchSuccess;
attr.onerror = OnFetchError;
attr.onprogress = OnFetchProgress;
attr.userData = &result;
// Set timeout
attr.timeoutMSecs = timeout_seconds_ * 1000;
// Prepare headers
std::vector<const char*> header_strings;
std::vector<std::string> header_storage;
for (const auto& [key, value] : headers) {
header_storage.push_back(key);
header_storage.push_back(value);
}
// Add Content-Type for POST/PUT if not provided
if ((method == "POST" || method == "PUT") && !body.empty()) {
bool has_content_type = false;
for (const auto& [key, value] : headers) {
if (key == "Content-Type") {
has_content_type = true;
break;
}
}
if (!has_content_type) {
header_storage.push_back("Content-Type");
header_storage.push_back("application/json");
}
}
// Convert to C-style array
for (const auto& str : header_storage) {
header_strings.push_back(str.c_str());
}
header_strings.push_back(nullptr); // Null-terminate
if (!header_strings.empty() && header_strings.size() > 1) {
attr.requestHeaders = header_strings.data();
}
// Set request body for POST/PUT
if (!body.empty() && (method == "POST" || method == "PUT")) {
attr.requestData = body.c_str();
attr.requestDataSize = body.length();
}
// Perform the fetch
emscripten_fetch_t* fetch = emscripten_fetch(&attr, url.c_str());
if (!fetch) {
return absl::InternalError("Failed to initiate fetch request");
}
// Wait for completion (convert async to sync)
{
std::unique_lock<std::mutex> lock(result.mutex);
result.cv.wait(lock, [&result] { return result.completed; });
}
// Check result
if (!result.success) {
return absl::UnavailableError(result.error_message.empty()
? "Fetch request failed"
: result.error_message);
}
// Build response
HttpResponse response;
response.status_code = result.status_code;
response.body = result.body;
response.headers = result.headers;
return response;
}
absl::StatusOr<HttpResponse> EmscriptenHttpClient::Get(
const std::string& url,
const Headers& headers) {
return PerformFetch("GET", url, "", headers);
}
absl::StatusOr<HttpResponse> EmscriptenHttpClient::Post(
const std::string& url,
const std::string& body,
const Headers& headers) {
return PerformFetch("POST", url, body, headers);
}
absl::StatusOr<HttpResponse> EmscriptenHttpClient::Put(
const std::string& url,
const std::string& body,
const Headers& headers) {
return PerformFetch("PUT", url, body, headers);
}
absl::StatusOr<HttpResponse> EmscriptenHttpClient::Delete(
const std::string& url,
const Headers& headers) {
return PerformFetch("DELETE", url, "", headers);
}
} // namespace net
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,122 @@
#ifndef YAZE_APP_NET_WASM_EMSCRIPTEN_HTTP_CLIENT_H_
#define YAZE_APP_NET_WASM_EMSCRIPTEN_HTTP_CLIENT_H_
#ifdef __EMSCRIPTEN__
#include <mutex>
#include <condition_variable>
#include <emscripten/fetch.h>
#include "app/net/http_client.h"
namespace yaze {
namespace net {
/**
* @class EmscriptenHttpClient
* @brief WASM HTTP client implementation using Emscripten Fetch API
*
* This implementation wraps the Emscripten fetch API for browser-based
* HTTP/HTTPS requests. All requests are subject to browser CORS policies.
*/
class EmscriptenHttpClient : public IHttpClient {
public:
EmscriptenHttpClient();
~EmscriptenHttpClient() override;
/**
* @brief Perform an HTTP GET request
* @param url The URL to request
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> Get(
const std::string& url,
const Headers& headers = {}) override;
/**
* @brief Perform an HTTP POST request
* @param url The URL to post to
* @param body The request body
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> Post(
const std::string& url,
const std::string& body,
const Headers& headers = {}) override;
/**
* @brief Perform an HTTP PUT request
* @param url The URL to put to
* @param body The request body
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> Put(
const std::string& url,
const std::string& body,
const Headers& headers = {}) override;
/**
* @brief Perform an HTTP DELETE request
* @param url The URL to delete
* @param headers Optional HTTP headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> Delete(
const std::string& url,
const Headers& headers = {}) override;
private:
/**
* @brief Structure to hold fetch result data
*/
struct FetchResult {
bool completed = false;
bool success = false;
int status_code = 0;
std::string body;
Headers headers;
std::string error_message;
std::mutex mutex;
std::condition_variable cv;
};
/**
* @brief Perform a fetch request
* @param method HTTP method
* @param url Request URL
* @param body Request body (for POST/PUT)
* @param headers Request headers
* @return HttpResponse or error status
*/
absl::StatusOr<HttpResponse> PerformFetch(
const std::string& method,
const std::string& url,
const std::string& body = "",
const Headers& headers = {});
/**
* @brief Success callback for fetch operations
*/
static void OnFetchSuccess(emscripten_fetch_t* fetch);
/**
* @brief Error callback for fetch operations
*/
static void OnFetchError(emscripten_fetch_t* fetch);
/**
* @brief Progress callback for fetch operations
*/
static void OnFetchProgress(emscripten_fetch_t* fetch);
};
} // namespace net
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_NET_WASM_EMSCRIPTEN_HTTP_CLIENT_H_

View File

@@ -0,0 +1,224 @@
#ifdef __EMSCRIPTEN__
#include "app/net/wasm/emscripten_websocket.h"
#include <cstring>
namespace yaze {
namespace net {
EmscriptenWebSocket::EmscriptenWebSocket()
: socket_(0), socket_valid_(false) {
state_ = WebSocketState::kDisconnected;
}
EmscriptenWebSocket::~EmscriptenWebSocket() {
if (socket_valid_ && state_ != WebSocketState::kDisconnected) {
Close();
}
}
EM_BOOL EmscriptenWebSocket::OnOpenCallback(
int eventType,
const EmscriptenWebSocketOpenEvent* websocketEvent,
void* userData) {
EmscriptenWebSocket* self = static_cast<EmscriptenWebSocket*>(userData);
self->state_ = WebSocketState::kConnected;
if (self->open_callback_) {
self->open_callback_();
}
return EM_TRUE;
}
EM_BOOL EmscriptenWebSocket::OnCloseCallback(
int eventType,
const EmscriptenWebSocketCloseEvent* websocketEvent,
void* userData) {
EmscriptenWebSocket* self = static_cast<EmscriptenWebSocket*>(userData);
self->state_ = WebSocketState::kClosed;
self->socket_valid_ = false;
if (self->close_callback_) {
self->close_callback_(websocketEvent->code,
websocketEvent->reason ? websocketEvent->reason : "");
}
self->state_ = WebSocketState::kDisconnected;
return EM_TRUE;
}
EM_BOOL EmscriptenWebSocket::OnErrorCallback(
int eventType,
const EmscriptenWebSocketErrorEvent* websocketEvent,
void* userData) {
EmscriptenWebSocket* self = static_cast<EmscriptenWebSocket*>(userData);
self->state_ = WebSocketState::kError;
if (self->error_callback_) {
self->error_callback_("WebSocket error occurred");
}
return EM_TRUE;
}
EM_BOOL EmscriptenWebSocket::OnMessageCallback(
int eventType,
const EmscriptenWebSocketMessageEvent* websocketEvent,
void* userData) {
EmscriptenWebSocket* self = static_cast<EmscriptenWebSocket*>(userData);
if (websocketEvent->isText) {
// Text message
if (self->message_callback_) {
// Convert UTF-8 data to string
std::string message(reinterpret_cast<const char*>(websocketEvent->data),
websocketEvent->numBytes);
self->message_callback_(message);
}
} else {
// Binary message
if (self->binary_message_callback_) {
self->binary_message_callback_(websocketEvent->data,
websocketEvent->numBytes);
}
}
return EM_TRUE;
}
absl::Status EmscriptenWebSocket::Connect(const std::string& url) {
if (state_ != WebSocketState::kDisconnected) {
return absl::FailedPreconditionError(
"WebSocket already connected or connecting");
}
state_ = WebSocketState::kConnecting;
url_ = url;
// Create WebSocket attributes
EmscriptenWebSocketCreateAttributes attrs = {
url.c_str(),
nullptr, // protocols (NULL = default)
EM_TRUE // createOnMainThread
};
// Create the WebSocket
socket_ = emscripten_websocket_new(&attrs);
if (socket_ <= 0) {
state_ = WebSocketState::kError;
return absl::InternalError("Failed to create WebSocket");
}
socket_valid_ = true;
// Set callbacks
EMSCRIPTEN_RESULT result;
result = emscripten_websocket_set_onopen_callback(
socket_, this, OnOpenCallback);
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
state_ = WebSocketState::kError;
socket_valid_ = false;
return absl::InternalError("Failed to set onopen callback");
}
result = emscripten_websocket_set_onclose_callback(
socket_, this, OnCloseCallback);
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
state_ = WebSocketState::kError;
socket_valid_ = false;
return absl::InternalError("Failed to set onclose callback");
}
result = emscripten_websocket_set_onerror_callback(
socket_, this, OnErrorCallback);
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
state_ = WebSocketState::kError;
socket_valid_ = false;
return absl::InternalError("Failed to set onerror callback");
}
result = emscripten_websocket_set_onmessage_callback(
socket_, this, OnMessageCallback);
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
state_ = WebSocketState::kError;
socket_valid_ = false;
return absl::InternalError("Failed to set onmessage callback");
}
// Connection is asynchronous in the browser
// The OnOpenCallback will be called when connected
return absl::OkStatus();
}
absl::Status EmscriptenWebSocket::Send(const std::string& message) {
if (state_ != WebSocketState::kConnected || !socket_valid_) {
return absl::FailedPreconditionError("WebSocket not connected");
}
EMSCRIPTEN_RESULT result = emscripten_websocket_send_utf8_text(
socket_, message.c_str());
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
return absl::InternalError("Failed to send text message");
}
return absl::OkStatus();
}
absl::Status EmscriptenWebSocket::SendBinary(const uint8_t* data,
size_t length) {
if (state_ != WebSocketState::kConnected || !socket_valid_) {
return absl::FailedPreconditionError("WebSocket not connected");
}
EMSCRIPTEN_RESULT result = emscripten_websocket_send_binary(
socket_, const_cast<void*>(static_cast<const void*>(data)), length);
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
return absl::InternalError("Failed to send binary message");
}
return absl::OkStatus();
}
absl::Status EmscriptenWebSocket::Close(int code, const std::string& reason) {
if (state_ == WebSocketState::kDisconnected ||
state_ == WebSocketState::kClosed ||
!socket_valid_) {
return absl::OkStatus();
}
state_ = WebSocketState::kClosing;
EMSCRIPTEN_RESULT result = emscripten_websocket_close(
socket_, code, reason.c_str());
if (result != EMSCRIPTEN_RESULT_SUCCESS) {
// Force state to closed even if close fails
state_ = WebSocketState::kClosed;
socket_valid_ = false;
return absl::InternalError("Failed to close WebSocket");
}
// OnCloseCallback will be called when the close completes
return absl::OkStatus();
}
} // namespace net
} // namespace yaze
#endif // __EMSCRIPTEN__

View File

@@ -0,0 +1,132 @@
#ifndef YAZE_APP_NET_WASM_EMSCRIPTEN_WEBSOCKET_H_
#define YAZE_APP_NET_WASM_EMSCRIPTEN_WEBSOCKET_H_
#ifdef __EMSCRIPTEN__
#include <emscripten/websocket.h>
#include "app/net/websocket_interface.h"
namespace yaze {
namespace net {
/**
* @class EmscriptenWebSocket
* @brief WASM WebSocket implementation using Emscripten WebSocket API
*
* This implementation wraps the Emscripten WebSocket API which provides
* direct access to the browser's native WebSocket implementation.
*/
class EmscriptenWebSocket : public IWebSocket {
public:
EmscriptenWebSocket();
~EmscriptenWebSocket() override;
/**
* @brief Connect to a WebSocket server
* @param url The WebSocket URL (ws:// or wss://)
* @return Status indicating success or failure
*/
absl::Status Connect(const std::string& url) override;
/**
* @brief Send a text message
* @param message The text message to send
* @return Status indicating success or failure
*/
absl::Status Send(const std::string& message) override;
/**
* @brief Send a binary message
* @param data The binary data to send
* @param length The length of the data
* @return Status indicating success or failure
*/
absl::Status SendBinary(const uint8_t* data, size_t length) override;
/**
* @brief Close the WebSocket connection
* @param code Optional close code
* @param reason Optional close reason
* @return Status indicating success or failure
*/
absl::Status Close(int code = 1000,
const std::string& reason = "") override;
/**
* @brief Get the current connection state
* @return Current WebSocket state
*/
WebSocketState GetState() const override { return state_; }
/**
* @brief Set callback for text message events
* @param callback Function to call when a text message is received
*/
void OnMessage(MessageCallback callback) override {
message_callback_ = callback;
}
/**
* @brief Set callback for binary message events
* @param callback Function to call when binary data is received
*/
void OnBinaryMessage(BinaryMessageCallback callback) override {
binary_message_callback_ = callback;
}
/**
* @brief Set callback for connection open events
* @param callback Function to call when connection is established
*/
void OnOpen(OpenCallback callback) override {
open_callback_ = callback;
}
/**
* @brief Set callback for connection close events
* @param callback Function to call when connection is closed
*/
void OnClose(CloseCallback callback) override {
close_callback_ = callback;
}
/**
* @brief Set callback for error events
* @param callback Function to call when an error occurs
*/
void OnError(ErrorCallback callback) override {
error_callback_ = callback;
}
private:
// Emscripten WebSocket callbacks (static, with user data)
static EM_BOOL OnOpenCallback(int eventType,
const EmscriptenWebSocketOpenEvent* websocketEvent,
void* userData);
static EM_BOOL OnCloseCallback(int eventType,
const EmscriptenWebSocketCloseEvent* websocketEvent,
void* userData);
static EM_BOOL OnErrorCallback(int eventType,
const EmscriptenWebSocketErrorEvent* websocketEvent,
void* userData);
static EM_BOOL OnMessageCallback(int eventType,
const EmscriptenWebSocketMessageEvent* websocketEvent,
void* userData);
// Emscripten WebSocket handle
EMSCRIPTEN_WEBSOCKET_T socket_;
// Track if socket is valid
bool socket_valid_;
};
} // namespace net
} // namespace yaze
#endif // __EMSCRIPTEN__
#endif // YAZE_APP_NET_WASM_EMSCRIPTEN_WEBSOCKET_H_

View File

@@ -8,7 +8,8 @@
#include "absl/strings/str_format.h"
// Cross-platform WebSocket support using httplib
#ifdef YAZE_WITH_JSON
// Skip httplib in WASM builds - use Emscripten WebSocket API instead
#if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__)
#ifndef _WIN32
#define CPPHTTPLIB_OPENSSL_SUPPORT
#endif
@@ -19,7 +20,8 @@ namespace yaze {
namespace net {
#ifdef YAZE_WITH_JSON
// Native (non-WASM) implementation using httplib
#if defined(YAZE_WITH_JSON) && !defined(__EMSCRIPTEN__)
// Platform-independent WebSocket implementation using httplib
class WebSocketClient::Impl {
@@ -151,6 +153,25 @@ class WebSocketClient::Impl {
std::function<void(const std::string&)> error_callback_;
};
#elif defined(__EMSCRIPTEN__)
// WASM stub - uses EmscriptenWebSocket from wasm/ directory instead
class WebSocketClient::Impl {
public:
absl::Status Connect(const std::string&, int) {
return absl::UnimplementedError(
"Use EmscriptenWebSocket for WASM WebSocket connections");
}
void Disconnect() {}
absl::Status Send(const std::string&) {
return absl::UnimplementedError(
"Use EmscriptenWebSocket for WASM WebSocket connections");
}
void SetMessageCallback(std::function<void(const std::string&)>) {}
void SetErrorCallback(std::function<void(const std::string&)>) {}
bool IsConnected() const { return false; }
};
#else
// Stub implementation when JSON is not available
@@ -168,7 +189,7 @@ class WebSocketClient::Impl {
bool IsConnected() const { return false; }
};
#endif // YAZE_WITH_JSON
#endif // YAZE_WITH_JSON && !__EMSCRIPTEN__
// ============================================================================
// WebSocketClient Implementation

View File

@@ -0,0 +1,160 @@
#ifndef YAZE_APP_NET_WEBSOCKET_INTERFACE_H_
#define YAZE_APP_NET_WEBSOCKET_INTERFACE_H_
#include <functional>
#include <string>
#include "absl/status/status.h"
namespace yaze {
namespace net {
/**
* @enum WebSocketState
* @brief WebSocket connection states
*/
enum class WebSocketState {
kDisconnected, // Not connected
kConnecting, // Connection in progress
kConnected, // Successfully connected
kClosing, // Close handshake in progress
kClosed, // Connection closed
kError // Error state
};
/**
* @class IWebSocket
* @brief Abstract interface for WebSocket client implementations
*
* This interface abstracts WebSocket operations to support both native
* (using various libraries) and WASM (using emscripten WebSocket) implementations.
* All methods use absl::Status for consistent error handling.
*/
class IWebSocket {
public:
// Callback types for WebSocket events
using MessageCallback = std::function<void(const std::string& message)>;
using BinaryMessageCallback = std::function<void(const uint8_t* data, size_t length)>;
using OpenCallback = std::function<void()>;
using CloseCallback = std::function<void(int code, const std::string& reason)>;
using ErrorCallback = std::function<void(const std::string& error)>;
virtual ~IWebSocket() = default;
/**
* @brief Connect to a WebSocket server
* @param url The WebSocket URL (ws:// or wss://)
* @return Status indicating success or failure
*/
virtual absl::Status Connect(const std::string& url) = 0;
/**
* @brief Send a text message
* @param message The text message to send
* @return Status indicating success or failure
*/
virtual absl::Status Send(const std::string& message) = 0;
/**
* @brief Send a binary message
* @param data The binary data to send
* @param length The length of the data
* @return Status indicating success or failure
*/
virtual absl::Status SendBinary(const uint8_t* data, size_t length) {
// Default implementation - can be overridden if binary is supported
return absl::UnimplementedError("Binary messages not implemented");
}
/**
* @brief Close the WebSocket connection
* @param code Optional close code (default: 1000 for normal closure)
* @param reason Optional close reason
* @return Status indicating success or failure
*/
virtual absl::Status Close(int code = 1000,
const std::string& reason = "") = 0;
/**
* @brief Get the current connection state
* @return Current WebSocket state
*/
virtual WebSocketState GetState() const = 0;
/**
* @brief Check if the WebSocket is connected
* @return true if connected, false otherwise
*/
virtual bool IsConnected() const {
return GetState() == WebSocketState::kConnected;
}
/**
* @brief Set callback for text message events
* @param callback Function to call when a text message is received
*/
virtual void OnMessage(MessageCallback callback) = 0;
/**
* @brief Set callback for binary message events
* @param callback Function to call when binary data is received
*/
virtual void OnBinaryMessage(BinaryMessageCallback callback) {
// Default implementation - can be overridden if binary is supported
binary_message_callback_ = callback;
}
/**
* @brief Set callback for connection open events
* @param callback Function to call when connection is established
*/
virtual void OnOpen(OpenCallback callback) = 0;
/**
* @brief Set callback for connection close events
* @param callback Function to call when connection is closed
*/
virtual void OnClose(CloseCallback callback) = 0;
/**
* @brief Set callback for error events
* @param callback Function to call when an error occurs
*/
virtual void OnError(ErrorCallback callback) = 0;
/**
* @brief Get the WebSocket URL
* @return The URL this socket is connected/connecting to
*/
virtual std::string GetUrl() const { return url_; }
/**
* @brief Set automatic reconnection
* @param enable Enable or disable auto-reconnect
* @param delay_seconds Delay between reconnection attempts
*/
virtual void SetAutoReconnect(bool enable, int delay_seconds = 5) {
auto_reconnect_ = enable;
reconnect_delay_seconds_ = delay_seconds;
}
protected:
std::string url_;
WebSocketState state_ = WebSocketState::kDisconnected;
// Callbacks (may be used by implementations)
MessageCallback message_callback_;
BinaryMessageCallback binary_message_callback_;
OpenCallback open_callback_;
CloseCallback close_callback_;
ErrorCallback error_callback_;
// Auto-reconnect settings
bool auto_reconnect_ = false;
int reconnect_delay_seconds_ = 5;
};
} // namespace net
} // namespace yaze
#endif // YAZE_APP_NET_WEBSOCKET_INTERFACE_H_