Something went wrong. Try again.
A Bluesky Playdate client
Something went wrong. Try again.
11 kB · 316 lines
Zig
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317const 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 typespub const HttpMethod = enum { GET, POST, PUT, DELETE, PATCH,};
// HTTP status code rangespub const HttpStatusCode = u16;
// Callback function typespub 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 configurationpub 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 statepub 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 statevar 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 allocationsvar g_headers_buffer: [512]u8 = undefined;
/// Convert HttpMethod enum to stringfn httpMethodToString(method: HttpMethod) []const u8 { return switch (method) { .GET => "GET", .POST => "POST", .PUT => "PUT", .DELETE => "DELETE", .PATCH => "PATCH", };}
/// Create HTTP connection with proper configurationpub 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 tokenpub 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 callbackspub 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 bufferfn 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 callbackfn 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 errorsfn 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 resourcesfn 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 statepub fn getNetworkState() NetworkState { return g_network_state.state;}
/// Check if network is idlepub fn isNetworkIdle() bool { return g_network_state.state == .idle;}
/// Force cleanup network resourcespub fn forceCleanup(playdate: *pdapi.PlaydateAPI) void { cleanup(playdate);}