const std = @import("std"); const pdapi = @import("playdate_api_definitions.zig"); pub const NetworkState = enum { idle, connecting, authenticating, requesting, fetching_posts, success, network_error, }; // HTTP method types pub const HttpMethod = enum { GET, POST, PUT, DELETE, PATCH, }; // HTTP status code ranges pub const HttpStatusCode = u16; // Callback function types pub const SuccessCallback = *const fn (status_code: HttpStatusCode, response_data: []const u8) void; pub const FailureCallback = *const fn (status_code: HttpStatusCode, error_message: []const u8) void; // Request configuration pub const HttpRequest = struct { method: HttpMethod, url: []const u8, server: []const u8, port: u16, path: []const u8, use_https: bool, bearer_token: ?[]const u8, body: ?[]const u8, response_buffer: []u8, success_callback: ?SuccessCallback, failure_callback: ?FailureCallback, }; // Network request state pub const NetworkRequestState = struct { state: NetworkState, connection: ?*pdapi.HTTPConnection, request: ?HttpRequest, bytes_read: usize, is_reading_chunks: bool, total_bytes_read: usize, expected_content_length: usize, network_access_requested: bool, }; // Global network state var g_network_state = NetworkRequestState{ .state = .idle, .connection = null, .request = null, .bytes_read = 0, .is_reading_chunks = false, .total_bytes_read = 0, .expected_content_length = 0, .network_access_requested = false, }; // Global buffers to avoid large stack allocations var g_headers_buffer: [512]u8 = undefined; /// Convert HttpMethod enum to string fn httpMethodToString(method: HttpMethod) []const u8 { return switch (method) { .GET => "GET", .POST => "POST", .PUT => "PUT", .DELETE => "DELETE", .PATCH => "PATCH", }; } /// Create HTTP connection with proper configuration pub fn createConnection(playdate: *pdapi.PlaydateAPI, server: []const u8, port: u16, use_https: bool) ?*pdapi.HTTPConnection { // Create a null-terminated buffer for the server name var server_buffer: [256]u8 = undefined; if (server.len >= server_buffer.len) { playdate.system.logToConsole("ERROR: Server name too long: %d", @as(c_int, @intCast(server.len))); return null; } @memcpy(server_buffer[0..server.len], server); server_buffer[server.len] = 0; playdate.system.logToConsole("Attempting to create connection to %s:%d", @as([*:0]const u8, @ptrCast(&server_buffer)), port); if (use_https) { playdate.system.logToConsole("Using HTTPS connection"); } else { playdate.system.logToConsole("Using HTTP connection"); } const connection = playdate.network.playdate_http.newConnection(@as([*:0]const u8, @ptrCast(&server_buffer)), port, use_https); // Set connection timeout - increase to 30 seconds for more reliability playdate.network.playdate_http.setConnectTimeout(connection, 30000); // 30 seconds playdate.network.playdate_http.setReadTimeout(connection, 30000); // 30 seconds playdate.system.logToConsole("HTTP connection created with 30s timeouts"); return connection; } /// Start an HTTP request with optional bearer token pub fn makeHttpRequest(playdate: *pdapi.PlaydateAPI, request: HttpRequest) bool { // Check if already processing a request if (g_network_state.state != .idle) { playdate.system.logToConsole("ERROR: Network request already in progress"); return false; } // Create connection const connection = createConnection(playdate, request.server, request.port, request.use_https); if (connection == null) { return false; } // Store request info g_network_state.connection = connection; g_network_state.request = request; g_network_state.state = .connecting; g_network_state.bytes_read = 0; g_network_state.total_bytes_read = 0; g_network_state.is_reading_chunks = false; // Build headers var headers_len: usize = 0; // Add Authorization header if bearer token provided if (request.bearer_token) |token| { const auth_header = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Authorization: Bearer {s}\r\n", .{token}) catch { playdate.system.logToConsole("ERROR: Failed to format authorization header"); return false; }; headers_len += auth_header.len; } // Add Content-Type header for POST/PUT/PATCH requests if (request.method == .POST or request.method == .PUT or request.method == .PATCH) { const content_type = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Content-Type: application/json\r\n", .{}) catch { playdate.system.logToConsole("ERROR: Failed to format content-type header"); return false; }; headers_len += content_type.len; } // Null terminate headers if (headers_len < g_headers_buffer.len) { g_headers_buffer[headers_len] = 0; } // Send request using query method const method_str = httpMethodToString(request.method); const headers_ptr = if (headers_len > 0) @as([*:0]const u8, @ptrCast(&g_headers_buffer)) else null; const body_ptr = if (request.body) |body| @as([*:0]const u8, @ptrCast(body.ptr)) else null; const body_len = if (request.body) |body| body.len else 0; const result = playdate.network.playdate_http.query(connection, @as([*:0]const u8, @ptrCast(method_str.ptr)), @as([*:0]const u8, @ptrCast(request.path.ptr)), headers_ptr, if (headers_len > 0) headers_len else 0, body_ptr, body_len); if (result != .NET_OK) { playdate.system.logToConsole("ERROR: Failed to send HTTP request"); g_network_state.state = .network_error; return false; } g_network_state.state = .requesting; playdate.system.logToConsole("HTTP %s request sent to %s%s", @as([*:0]const u8, @ptrCast(method_str.ptr)), @as([*:0]const u8, @ptrCast(request.server.ptr)), @as([*:0]const u8, @ptrCast(request.path.ptr))); return true; } /// Process network responses and handle callbacks pub fn updateNetwork(playdate: *pdapi.PlaydateAPI) void { if (g_network_state.state == .idle or g_network_state.connection == null) { return; } const connection = g_network_state.connection.?; _ = g_network_state.request.?; // Check for errors const net_error = playdate.network.playdate_http.getError(connection); if (net_error != .NET_OK) { playdate.system.logToConsole("ERROR: HTTP network error: %d", @intFromEnum(net_error)); handleNetworkError(playdate, "Network error"); return; } // Read available data if we're in requesting state if (g_network_state.state == .requesting) { const bytes_available = playdate.network.playdate_http.getBytesAvailable(connection); if (bytes_available > 0) { readResponseData(playdate); } else { // Check if request is complete by trying to get response status const status_code = playdate.network.playdate_http.getResponseStatus(connection); if (status_code > 0) { // We have a response, process it processResponse(playdate); } } } } /// Read response data into the provided buffer fn readResponseData(playdate: *pdapi.PlaydateAPI) void { const connection = g_network_state.connection.?; const request = g_network_state.request.?; const available_space = request.response_buffer.len - g_network_state.bytes_read; if (available_space == 0) { playdate.system.logToConsole("WARNING: Response buffer full, stopping read"); processResponse(playdate); return; } const bytes_to_read = @min(available_space, 1024); // Read in chunks of 1KB or less const buffer_ptr = request.response_buffer[g_network_state.bytes_read .. g_network_state.bytes_read + bytes_to_read]; const bytes_read = playdate.network.playdate_http.read(connection, @as([*]u8, @ptrCast(buffer_ptr.ptr)), @intCast(bytes_to_read)); if (bytes_read > 0) { g_network_state.bytes_read += @intCast(bytes_read); g_network_state.total_bytes_read += @intCast(bytes_read); playdate.system.logToConsole("Read %d bytes, total: %d", bytes_read, g_network_state.total_bytes_read); } } /// Process the completed response and call appropriate callback fn processResponse(playdate: *pdapi.PlaydateAPI) void { const connection = g_network_state.connection.?; const request = g_network_state.request.?; // Get HTTP status code const status_code = playdate.network.playdate_http.getResponseStatus(connection); playdate.system.logToConsole("HTTP response status: %d", status_code); // Null-terminate response data if (g_network_state.bytes_read < request.response_buffer.len) { request.response_buffer[g_network_state.bytes_read] = 0; } const response_data = request.response_buffer[0..g_network_state.bytes_read]; // Determine if this is a success (2xx) or failure (4xx/5xx) if (status_code >= 200 and status_code < 300) { // Success (2xx) g_network_state.state = .success; if (request.success_callback) |callback| { callback(@intCast(status_code), response_data); } } else if (status_code >= 400) { // Client/Server error (4xx/5xx) g_network_state.state = .network_error; if (request.failure_callback) |callback| { callback(@intCast(status_code), response_data); } } else { // Other status codes (1xx, 3xx) - treat as success for now g_network_state.state = .success; if (request.success_callback) |callback| { callback(@intCast(status_code), response_data); } } // Cleanup cleanup(playdate); } /// Handle network errors fn handleNetworkError(playdate: *pdapi.PlaydateAPI, error_message: []const u8) void { g_network_state.state = .network_error; if (g_network_state.request) |request| { if (request.failure_callback) |callback| { callback(0, error_message); // Status code 0 indicates network error } } cleanup(playdate); } /// Clean up network resources fn cleanup(playdate: *pdapi.PlaydateAPI) void { if (g_network_state.connection) |connection| { playdate.network.playdate_http.close(connection); g_network_state.connection = null; } g_network_state.request = null; g_network_state.bytes_read = 0; g_network_state.total_bytes_read = 0; g_network_state.is_reading_chunks = false; g_network_state.state = .idle; } /// Get current network state pub fn getNetworkState() NetworkState { return g_network_state.state; } /// Check if network is idle pub fn isNetworkIdle() bool { return g_network_state.state == .idle; } /// Force cleanup network resources pub fn forceCleanup(playdate: *pdapi.PlaydateAPI) void { cleanup(playdate); }