GET /xrpc/app.bsky.actor.searchActorsTypeahead typeahead.waow.tech
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 5080912ca9c3d7d4301e4dce83a5b804de56d00a 224 lines 6.8 kB view raw
1//! Read-only Turso HTTP API client for sync 2//! Uses hrana v2 pipeline protocol 3 4const std = @import("std"); 5const http = std.http; 6const json = std.json; 7const mem = std.mem; 8const Allocator = mem.Allocator; 9 10const log = std.log.scoped(.turso); 11 12const TursoClient = @This(); 13 14const Value = struct { type: []const u8 = "text", value: []const u8 }; 15const Stmt = struct { sql: []const u8, args: ?[]const Value = null }; 16const ExecuteReq = struct { type: []const u8 = "execute", stmt: Stmt }; 17const CloseReq = struct { type: []const u8 = "close" }; 18 19allocator: Allocator, 20url: []const u8, // host only (no protocol prefix) 21token: []const u8, 22http_client: http.Client, 23mutex: std.Thread.Mutex = .{}, 24 25pub fn init(allocator: Allocator) !TursoClient { 26 const url = std.posix.getenv("TURSO_URL") orelse { 27 log.err("TURSO_URL not set", .{}); 28 return error.MissingEnv; 29 }; 30 const token = std.posix.getenv("TURSO_AUTH_TOKEN") orelse { 31 log.err("TURSO_AUTH_TOKEN not set", .{}); 32 return error.MissingEnv; 33 }; 34 35 const libsql_prefix = "libsql://"; 36 const host = if (mem.startsWith(u8, url, libsql_prefix)) 37 url[libsql_prefix.len..] 38 else 39 url; 40 41 log.info("turso client → {s}", .{host}); 42 43 return .{ 44 .allocator = allocator, 45 .url = host, 46 .token = token, 47 .http_client = .{ .allocator = allocator }, 48 }; 49} 50 51pub fn deinit(self: *TursoClient) void { 52 self.http_client.deinit(); 53} 54 55pub const Row = struct { 56 columns: []const json.Value, 57 58 pub fn text(self: Row, index: usize) []const u8 { 59 if (index >= self.columns.len) return ""; 60 return extractText(self.columns[index]); 61 } 62 63 pub fn int(self: Row, index: usize) i64 { 64 if (index >= self.columns.len) return 0; 65 return extractInt(self.columns[index]); 66 } 67}; 68 69pub const Result = struct { 70 allocator: Allocator, 71 parsed: ?json.Parsed(json.Value), 72 rows: []const Row, 73 74 pub fn deinit(self: *Result) void { 75 self.allocator.free(self.rows); 76 if (self.parsed) |*p| p.deinit(); 77 } 78}; 79 80pub fn query(self: *TursoClient, sql: []const u8, args: []const []const u8) !Result { 81 const response = try self.executeRaw(sql, args); 82 defer self.allocator.free(response); 83 return parseResult(self.allocator, response); 84} 85 86fn executeRaw(self: *TursoClient, sql: []const u8, args: []const []const u8) ![]const u8 { 87 self.mutex.lock(); 88 defer self.mutex.unlock(); 89 90 var url_buf: [512]u8 = undefined; 91 const url = std.fmt.bufPrint(&url_buf, "https://{s}/v2/pipeline", .{self.url}) catch 92 return error.UrlTooLong; 93 94 const body = try self.buildRequestBody(sql, args); 95 defer self.allocator.free(body); 96 97 var auth_buf: [512]u8 = undefined; 98 const auth = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{self.token}) catch 99 return error.AuthTooLong; 100 101 var response_body: std.Io.Writer.Allocating = .init(self.allocator); 102 errdefer response_body.deinit(); 103 104 const res = self.http_client.fetch(.{ 105 .location = .{ .url = url }, 106 .method = .POST, 107 .headers = .{ 108 .content_type = .{ .override = "application/json" }, 109 .authorization = .{ .override = auth }, 110 }, 111 .payload = body, 112 .response_writer = &response_body.writer, 113 }) catch |err| { 114 log.err("http failed: {s}", .{@errorName(err)}); 115 return error.HttpError; 116 }; 117 118 if (res.status != .ok) { 119 const resp_text = response_body.toOwnedSlice() catch ""; 120 defer if (resp_text.len > 0) self.allocator.free(resp_text); 121 const preview = if (resp_text.len > 200) resp_text[0..200] else resp_text; 122 log.err("turso error: {} | {s}", .{ res.status, preview }); 123 return error.TursoError; 124 } 125 126 return try response_body.toOwnedSlice(); 127} 128 129fn buildRequestBody(self: *TursoClient, sql: []const u8, args: []const []const u8) ![]const u8 { 130 var body: std.Io.Writer.Allocating = .init(self.allocator); 131 errdefer body.deinit(); 132 var jw: json.Stringify = .{ .writer = &body.writer, .options = .{ .emit_null_optional_fields = false } }; 133 134 var values: []const Value = &.{}; 135 defer if (values.len > 0) self.allocator.free(values); 136 137 if (args.len > 0) { 138 const v = try self.allocator.alloc(Value, args.len); 139 for (args, 0..) |arg, i| { 140 v[i] = .{ .value = arg }; 141 } 142 values = v; 143 } 144 145 try jw.beginObject(); 146 try jw.objectField("requests"); 147 try jw.beginArray(); 148 try jw.write(ExecuteReq{ 149 .stmt = .{ .sql = sql, .args = if (values.len > 0) values else null }, 150 }); 151 try jw.write(CloseReq{}); 152 try jw.endArray(); 153 try jw.endObject(); 154 155 return try body.toOwnedSlice(); 156} 157 158fn parseResult(allocator: Allocator, response: []const u8) !Result { 159 const parsed = json.parseFromSlice(json.Value, allocator, response, .{}) catch { 160 return .{ .allocator = allocator, .parsed = null, .rows = &.{} }; 161 }; 162 163 const json_rows = getRowsFromParsed(parsed.value) orelse { 164 return .{ .allocator = allocator, .parsed = parsed, .rows = &.{} }; 165 }; 166 167 var rows: std.ArrayList(Row) = .{}; 168 errdefer rows.deinit(allocator); 169 170 for (json_rows.items) |item| { 171 if (item == .array) { 172 try rows.append(allocator, .{ .columns = item.array.items }); 173 } 174 } 175 176 return .{ 177 .allocator = allocator, 178 .parsed = parsed, 179 .rows = try rows.toOwnedSlice(allocator), 180 }; 181} 182 183fn getRowsFromResult(item: json.Value) ?json.Array { 184 if (item != .object) return null; 185 const resp = item.object.get("response") orelse return null; 186 if (resp != .object) return null; 187 const res = resp.object.get("result") orelse return null; 188 if (res != .object) return null; 189 const rows = res.object.get("rows") orelse return null; 190 if (rows != .array) return null; 191 return rows.array; 192} 193 194fn getRowsFromParsed(value: json.Value) ?json.Array { 195 const results = value.object.get("results") orelse return null; 196 if (results != .array or results.array.items.len == 0) return null; 197 return getRowsFromResult(results.array.items[0]); 198} 199 200fn extractText(val: json.Value) []const u8 { 201 return switch (val) { 202 .string => |s| s, 203 .object => |obj| { 204 const v = obj.get("value") orelse return ""; 205 return if (v == .string) v.string else ""; 206 }, 207 else => "", 208 }; 209} 210 211fn extractInt(val: json.Value) i64 { 212 return switch (val) { 213 .integer => |i| i, 214 .object => |obj| { 215 const v = obj.get("value") orelse return 0; 216 return switch (v) { 217 .integer => |i| i, 218 .string => |s| std.fmt.parseInt(i64, s, 10) catch 0, 219 else => 0, 220 }; 221 }, 222 else => 0, 223 }; 224}