A Bluesky Playdate client
1const std = @import("std");
2const pdapi = @import("playdate_api_definitions.zig");
3
4pub const NetworkState = enum {
5 idle,
6 connecting,
7 authenticating,
8 requesting,
9 fetching_posts,
10 success,
11 network_error,
12};
13
14// HTTP method types
15pub const HttpMethod = enum {
16 GET,
17 POST,
18 PUT,
19 DELETE,
20 PATCH,
21};
22
23// HTTP status code ranges
24pub const HttpStatusCode = u16;
25
26// Callback function types
27pub const SuccessCallback = *const fn (status_code: HttpStatusCode, response_data: []const u8) void;
28pub const FailureCallback = *const fn (status_code: HttpStatusCode, error_message: []const u8) void;
29
30// Request configuration
31pub const HttpRequest = struct {
32 method: HttpMethod,
33 url: []const u8,
34 server: []const u8,
35 port: u16,
36 path: []const u8,
37 use_https: bool,
38 bearer_token: ?[]const u8,
39 body: ?[]const u8,
40 response_buffer: []u8,
41 success_callback: ?SuccessCallback,
42 failure_callback: ?FailureCallback,
43};
44
45// Network request state
46pub const NetworkRequestState = struct {
47 state: NetworkState,
48 connection: ?*pdapi.HTTPConnection,
49 request: ?HttpRequest,
50 bytes_read: usize,
51 is_reading_chunks: bool,
52 total_bytes_read: usize,
53 expected_content_length: usize,
54 network_access_requested: bool,
55};
56
57// Global network state
58var g_network_state = NetworkRequestState{
59 .state = .idle,
60 .connection = null,
61 .request = null,
62 .bytes_read = 0,
63 .is_reading_chunks = false,
64 .total_bytes_read = 0,
65 .expected_content_length = 0,
66 .network_access_requested = false,
67};
68
69// Global buffers to avoid large stack allocations
70var g_headers_buffer: [512]u8 = undefined;
71
72/// Convert HttpMethod enum to string
73fn httpMethodToString(method: HttpMethod) []const u8 {
74 return switch (method) {
75 .GET => "GET",
76 .POST => "POST",
77 .PUT => "PUT",
78 .DELETE => "DELETE",
79 .PATCH => "PATCH",
80 };
81}
82
83/// Create HTTP connection with proper configuration
84pub fn createConnection(playdate: *pdapi.PlaydateAPI, server: []const u8, port: u16, use_https: bool) ?*pdapi.HTTPConnection {
85 // Create a null-terminated buffer for the server name
86 var server_buffer: [256]u8 = undefined;
87 if (server.len >= server_buffer.len) {
88 playdate.system.logToConsole("ERROR: Server name too long: %d", @as(c_int, @intCast(server.len)));
89 return null;
90 }
91 @memcpy(server_buffer[0..server.len], server);
92 server_buffer[server.len] = 0;
93
94 playdate.system.logToConsole("Attempting to create connection to %s:%d", @as([*:0]const u8, @ptrCast(&server_buffer)), port);
95 if (use_https) {
96 playdate.system.logToConsole("Using HTTPS connection");
97 } else {
98 playdate.system.logToConsole("Using HTTP connection");
99 }
100 const connection = playdate.network.playdate_http.newConnection(@as([*:0]const u8, @ptrCast(&server_buffer)), port, use_https);
101
102 // Set connection timeout - increase to 30 seconds for more reliability
103 playdate.network.playdate_http.setConnectTimeout(connection, 30000); // 30 seconds
104 playdate.network.playdate_http.setReadTimeout(connection, 30000); // 30 seconds
105 playdate.system.logToConsole("HTTP connection created with 30s timeouts");
106
107 return connection;
108}
109
110/// Start an HTTP request with optional bearer token
111pub fn makeHttpRequest(playdate: *pdapi.PlaydateAPI, request: HttpRequest) bool {
112 // Check if already processing a request
113 if (g_network_state.state != .idle) {
114 playdate.system.logToConsole("ERROR: Network request already in progress");
115 return false;
116 }
117
118 // Create connection
119 const connection = createConnection(playdate, request.server, request.port, request.use_https);
120 if (connection == null) {
121 return false;
122 }
123
124 // Store request info
125 g_network_state.connection = connection;
126 g_network_state.request = request;
127 g_network_state.state = .connecting;
128 g_network_state.bytes_read = 0;
129 g_network_state.total_bytes_read = 0;
130 g_network_state.is_reading_chunks = false;
131
132 // Build headers
133 var headers_len: usize = 0;
134
135 // Add Authorization header if bearer token provided
136 if (request.bearer_token) |token| {
137 const auth_header = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Authorization: Bearer {s}\r\n", .{token}) catch {
138 playdate.system.logToConsole("ERROR: Failed to format authorization header");
139 return false;
140 };
141 headers_len += auth_header.len;
142 }
143
144 // Add Content-Type header for POST/PUT/PATCH requests
145 if (request.method == .POST or request.method == .PUT or request.method == .PATCH) {
146 const content_type = std.fmt.bufPrint(g_headers_buffer[headers_len..], "Content-Type: application/json\r\n", .{}) catch {
147 playdate.system.logToConsole("ERROR: Failed to format content-type header");
148 return false;
149 };
150 headers_len += content_type.len;
151 }
152
153 // Null terminate headers
154 if (headers_len < g_headers_buffer.len) {
155 g_headers_buffer[headers_len] = 0;
156 }
157
158 // Send request using query method
159 const method_str = httpMethodToString(request.method);
160 const headers_ptr = if (headers_len > 0) @as([*:0]const u8, @ptrCast(&g_headers_buffer)) else null;
161 const body_ptr = if (request.body) |body| @as([*:0]const u8, @ptrCast(body.ptr)) else null;
162 const body_len = if (request.body) |body| body.len else 0;
163
164 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);
165
166 if (result != .NET_OK) {
167 playdate.system.logToConsole("ERROR: Failed to send HTTP request");
168 g_network_state.state = .network_error;
169 return false;
170 }
171
172 g_network_state.state = .requesting;
173 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)));
174
175 return true;
176}
177
178/// Process network responses and handle callbacks
179pub fn updateNetwork(playdate: *pdapi.PlaydateAPI) void {
180 if (g_network_state.state == .idle or g_network_state.connection == null) {
181 return;
182 }
183
184 const connection = g_network_state.connection.?;
185 _ = g_network_state.request.?;
186
187 // Check for errors
188 const net_error = playdate.network.playdate_http.getError(connection);
189 if (net_error != .NET_OK) {
190 playdate.system.logToConsole("ERROR: HTTP network error: %d", @intFromEnum(net_error));
191 handleNetworkError(playdate, "Network error");
192 return;
193 }
194
195 // Read available data if we're in requesting state
196 if (g_network_state.state == .requesting) {
197 const bytes_available = playdate.network.playdate_http.getBytesAvailable(connection);
198 if (bytes_available > 0) {
199 readResponseData(playdate);
200 } else {
201 // Check if request is complete by trying to get response status
202 const status_code = playdate.network.playdate_http.getResponseStatus(connection);
203 if (status_code > 0) {
204 // We have a response, process it
205 processResponse(playdate);
206 }
207 }
208 }
209}
210
211/// Read response data into the provided buffer
212fn readResponseData(playdate: *pdapi.PlaydateAPI) void {
213 const connection = g_network_state.connection.?;
214 const request = g_network_state.request.?;
215 const available_space = request.response_buffer.len - g_network_state.bytes_read;
216
217 if (available_space == 0) {
218 playdate.system.logToConsole("WARNING: Response buffer full, stopping read");
219 processResponse(playdate);
220 return;
221 }
222
223 const bytes_to_read = @min(available_space, 1024); // Read in chunks of 1KB or less
224 const buffer_ptr = request.response_buffer[g_network_state.bytes_read .. g_network_state.bytes_read + bytes_to_read];
225
226 const bytes_read = playdate.network.playdate_http.read(connection, @as([*]u8, @ptrCast(buffer_ptr.ptr)), @intCast(bytes_to_read));
227
228 if (bytes_read > 0) {
229 g_network_state.bytes_read += @intCast(bytes_read);
230 g_network_state.total_bytes_read += @intCast(bytes_read);
231 playdate.system.logToConsole("Read %d bytes, total: %d", bytes_read, g_network_state.total_bytes_read);
232 }
233}
234
235/// Process the completed response and call appropriate callback
236fn processResponse(playdate: *pdapi.PlaydateAPI) void {
237 const connection = g_network_state.connection.?;
238 const request = g_network_state.request.?;
239
240 // Get HTTP status code
241 const status_code = playdate.network.playdate_http.getResponseStatus(connection);
242 playdate.system.logToConsole("HTTP response status: %d", status_code);
243
244 // Null-terminate response data
245 if (g_network_state.bytes_read < request.response_buffer.len) {
246 request.response_buffer[g_network_state.bytes_read] = 0;
247 }
248
249 const response_data = request.response_buffer[0..g_network_state.bytes_read];
250
251 // Determine if this is a success (2xx) or failure (4xx/5xx)
252 if (status_code >= 200 and status_code < 300) {
253 // Success (2xx)
254 g_network_state.state = .success;
255 if (request.success_callback) |callback| {
256 callback(@intCast(status_code), response_data);
257 }
258 } else if (status_code >= 400) {
259 // Client/Server error (4xx/5xx)
260 g_network_state.state = .network_error;
261 if (request.failure_callback) |callback| {
262 callback(@intCast(status_code), response_data);
263 }
264 } else {
265 // Other status codes (1xx, 3xx) - treat as success for now
266 g_network_state.state = .success;
267 if (request.success_callback) |callback| {
268 callback(@intCast(status_code), response_data);
269 }
270 }
271
272 // Cleanup
273 cleanup(playdate);
274}
275
276/// Handle network errors
277fn handleNetworkError(playdate: *pdapi.PlaydateAPI, error_message: []const u8) void {
278 g_network_state.state = .network_error;
279
280 if (g_network_state.request) |request| {
281 if (request.failure_callback) |callback| {
282 callback(0, error_message); // Status code 0 indicates network error
283 }
284 }
285
286 cleanup(playdate);
287}
288
289/// Clean up network resources
290fn cleanup(playdate: *pdapi.PlaydateAPI) void {
291 if (g_network_state.connection) |connection| {
292 playdate.network.playdate_http.close(connection);
293 g_network_state.connection = null;
294 }
295
296 g_network_state.request = null;
297 g_network_state.bytes_read = 0;
298 g_network_state.total_bytes_read = 0;
299 g_network_state.is_reading_chunks = false;
300 g_network_state.state = .idle;
301}
302
303/// Get current network state
304pub fn getNetworkState() NetworkState {
305 return g_network_state.state;
306}
307
308/// Check if network is idle
309pub fn isNetworkIdle() bool {
310 return g_network_state.state == .idle;
311}
312
313/// Force cleanup network resources
314pub fn forceCleanup(playdate: *pdapi.PlaydateAPI) void {
315 cleanup(playdate);
316}