#include "fl/fetch.h" #include "fl/warn.h" #include "fl/str.h" #include "fl/mutex.h" #include "fl/singleton.h" #include "fl/engine_events.h" #include "fl/async.h" #ifdef __EMSCRIPTEN__ #include #include #endif // Include WASM-specific implementation #include "platforms/wasm/js_fetch.h" namespace fl { #ifdef __EMSCRIPTEN__ // ========== WASM Implementation using JavaScript fetch ========== // Promise storage moved to FetchManager singleton // Use existing WASM fetch infrastructure void fetch(const fl::string& url, const FetchCallback& callback) { // Use the existing WASM fetch implementation - no conversion needed since both use fl::response wasm_fetch.get(url).response(callback); } // Internal helper to execute a fetch request and return a promise fl::promise execute_fetch_request(const fl::string& url, const fetch_options& request) { // Create a promise for this request auto promise = fl::promise::create(); // Register with fetch manager to ensure it's tracked FetchManager::instance().register_promise(promise); // Get the actual URL to use (use request URL if provided, otherwise use parameter URL) fl::string fetch_url = request.url().empty() ? url : request.url(); // Convert our request to the existing WASM fetch system auto wasm_request = WasmFetchRequest(fetch_url); // Use lambda that captures the promise directly (shared_ptr is safe to copy) // Make the lambda mutable so we can call non-const methods on the captured promise wasm_request.response([promise](const response& resp) mutable { // Complete the promise directly - no need for double storage if (promise.valid()) { promise.complete_with_value(resp); } }); return promise; } #else // ========== Embedded/Stub Implementation ========== void fetch(const fl::string& url, const FetchCallback& callback) { (void)url; // Unused in stub implementation // For embedded platforms, immediately call callback with a "not supported" response response resp(501, "Not Implemented"); resp.set_text("HTTP fetch not supported on this platform"); callback(resp); } // Internal helper to execute a fetch request and return a promise fl::promise execute_fetch_request(const fl::string& url, const fetch_options& request) { (void)request; // Unused in stub implementation FL_WARN("HTTP fetch is not supported on non-WASM platforms. URL: " << url); // Create error response response error_response(501, "Not Implemented"); error_response.set_body("HTTP fetch is only available in WASM/browser builds. This platform does not support network requests."); // Create resolved promise with error response auto promise = fl::promise::resolve(error_response); return promise; } #endif // ========== Engine Events Integration ========== // ========== Promise-Based API Implementation ========== class FetchEngineListener : public EngineEvents::Listener { public: FetchEngineListener() { EngineEvents::addListener(this); }; ~FetchEngineListener() override { // Listener base class automatically removes itself EngineEvents::removeListener(this); } void onEndFrame() override { // Update all async tasks (fetch, timers, etc.) at the end of each frame fl::async_run(); } }; FetchManager& FetchManager::instance() { return fl::Singleton::instance(); } void FetchManager::register_promise(const fl::promise& promise) { // Auto-register with async system and engine listener on first promise if (mActivePromises.empty()) { AsyncManager::instance().register_runner(this); if (!mEngineListener) { mEngineListener = fl::make_unique(); EngineEvents::addListener(mEngineListener.get()); } } mActivePromises.push_back(promise); } void FetchManager::update() { // Update all active promises first for (auto& promise : mActivePromises) { if (promise.valid()) { promise.update(); } } // Then clean up completed/invalid promises in a separate pass cleanup_completed_promises(); // Auto-unregister from async system when no more promises if (mActivePromises.empty()) { AsyncManager::instance().unregister_runner(this); if (mEngineListener) { EngineEvents::removeListener(mEngineListener.get()); mEngineListener.reset(); } } } bool FetchManager::has_active_tasks() const { return !mActivePromises.empty(); } size_t FetchManager::active_task_count() const { return mActivePromises.size(); } fl::size FetchManager::active_requests() const { return mActivePromises.size(); } void FetchManager::cleanup_completed_promises() { // Rebuild vector without completed promises fl::vector> active_promises; for (const auto& promise : mActivePromises) { if (promise.valid() && !promise.is_completed()) { active_promises.push_back(promise); } } mActivePromises = fl::move(active_promises); } // WASM promise management methods removed - no longer needed // Promises are now handled directly via shared_ptr capture in callbacks // ========== Public API Functions ========== fl::promise fetch_get(const fl::string& url, const fetch_options& request) { // Create a new request with GET method fetch_options get_request(url, RequestOptions("GET")); // Apply any additional options from the provided request const auto& opts = request.options(); get_request.timeout(opts.timeout_ms); for (const auto& header : opts.headers) { get_request.header(header.first, header.second); } if (!opts.body.empty()) { get_request.body(opts.body); } return execute_fetch_request(url, get_request); } fl::promise fetch_post(const fl::string& url, const fetch_options& request) { // Create a new request with POST method fetch_options post_request(url, RequestOptions("POST")); // Apply any additional options from the provided request const auto& opts = request.options(); post_request.timeout(opts.timeout_ms); for (const auto& header : opts.headers) { post_request.header(header.first, header.second); } if (!opts.body.empty()) { post_request.body(opts.body); } return execute_fetch_request(url, post_request); } fl::promise fetch_put(const fl::string& url, const fetch_options& request) { // Create a new request with PUT method fetch_options put_request(url, RequestOptions("PUT")); // Apply any additional options from the provided request const auto& opts = request.options(); put_request.timeout(opts.timeout_ms); for (const auto& header : opts.headers) { put_request.header(header.first, header.second); } if (!opts.body.empty()) { put_request.body(opts.body); } return execute_fetch_request(url, put_request); } fl::promise fetch_delete(const fl::string& url, const fetch_options& request) { // Create a new request with DELETE method fetch_options delete_request(url, RequestOptions("DELETE")); // Apply any additional options from the provided request const auto& opts = request.options(); delete_request.timeout(opts.timeout_ms); for (const auto& header : opts.headers) { delete_request.header(header.first, header.second); } if (!opts.body.empty()) { delete_request.body(opts.body); } return execute_fetch_request(url, delete_request); } fl::promise fetch_head(const fl::string& url, const fetch_options& request) { // Create a new request with HEAD method fetch_options head_request(url, RequestOptions("HEAD")); // Apply any additional options from the provided request const auto& opts = request.options(); head_request.timeout(opts.timeout_ms); for (const auto& header : opts.headers) { head_request.header(header.first, header.second); } if (!opts.body.empty()) { head_request.body(opts.body); } return execute_fetch_request(url, head_request); } fl::promise fetch_http_options(const fl::string& url, const fetch_options& request) { // Create a new request with OPTIONS method fetch_options options_request(url, RequestOptions("OPTIONS")); // Apply any additional options from the provided request const auto& opts = request.options(); options_request.timeout(opts.timeout_ms); for (const auto& header : opts.headers) { options_request.header(header.first, header.second); } if (!opts.body.empty()) { options_request.body(opts.body); } return execute_fetch_request(url, options_request); } fl::promise fetch_patch(const fl::string& url, const fetch_options& request) { // Create a new request with PATCH method fetch_options patch_request(url, RequestOptions("PATCH")); // Apply any additional options from the provided request const auto& opts = request.options(); patch_request.timeout(opts.timeout_ms); for (const auto& header : opts.headers) { patch_request.header(header.first, header.second); } if (!opts.body.empty()) { patch_request.body(opts.body); } return execute_fetch_request(url, patch_request); } fl::promise fetch_request(const fl::string& url, const RequestOptions& options) { // Create a fetch_options with the provided options fetch_options request(url, options); // Use the helper function to execute the request return execute_fetch_request(url, request); } void fetch_update() { // Legacy function - use fl::async_run() for new code // This provides backwards compatibility for existing code fl::async_run(); } fl::size fetch_active_requests() { return FetchManager::instance().active_requests(); } // ========== Response Class Method Implementations ========== fl::Json response::json() const { if (!mJsonParsed) { if (is_json() || mBody.find("{") != fl::string::npos || mBody.find("[") != fl::string::npos) { mCachedJson = parse_json_body(); } else { FL_WARN("Response is not JSON: " << mBody); mCachedJson = fl::Json(nullptr); // Not JSON content } mJsonParsed = true; } return mCachedJson.has_value() ? *mCachedJson : fl::Json(nullptr); } } // namespace fl