adopt zat library for atproto primitives

- replace hand-rolled TID parsing with zat.internal.Tid
- replace manual AT-URI parsing with zat.internal.AtUri
- cleaner, more correct (zat validates TID first char high bit)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Changed files
+19 -20
src
+6
build.zig
··· 14 14 .optimize = optimize, 15 15 }); 16 16 17 + const zat = b.dependency("zat", .{ 18 + .target = target, 19 + .optimize = optimize, 20 + }); 21 + 17 22 const exe = b.addExecutable(.{ 18 23 .name = "zig-bsky-feed", 19 24 .root_module = b.createModule(.{ ··· 23 28 .imports = &.{ 24 29 .{ .name = "websocket", .module = websocket.module("websocket") }, 25 30 .{ .name = "zqlite", .module = zqlite.module("zqlite") }, 31 + .{ .name = "zat", .module = zat.module("zat") }, 26 32 }, 27 33 }), 28 34 });
+4
build.zig.zon
··· 12 12 .url = "https://github.com/karlseguin/zqlite.zig/archive/refs/heads/master.tar.gz", 13 13 .hash = "zqlite-0.0.0-RWLaY_y_mADh2LdbDrG_2HT2dBAcsAR8Jig_7-dOJd0B", 14 14 }, 15 + .zat = .{ 16 + .url = "https://github.com/zzstoatzz/zat/archive/refs/heads/main.tar.gz", 17 + .hash = "zat-0.0.1-alpha-5PuC7liTAAAHY5R9yFiTz2Q51azmq4gLHpJLrzlA6-ms", 18 + }, 15 19 }, 16 20 .paths = .{ 17 21 "build.zig",
+1
src/atproto.zig
··· 5 5 const net = std.net; 6 6 const tls = std.crypto.tls; 7 7 const Allocator = mem.Allocator; 8 + const zat = @import("zat"); 8 9 9 10 // ============================================================================= 10 11 // atproto utilities
+8 -20
src/db.zig
··· 1 1 const std = @import("std"); 2 2 const posix = std.posix; 3 3 const zqlite = @import("zqlite"); 4 + const zat = @import("zat"); 4 5 const config = @import("config.zig"); 5 6 const atproto = @import("atproto.zig"); 6 7 ··· 106 107 }; 107 108 } 108 109 109 - /// parse TID (base32-sortable) to get timestamp in milliseconds 110 - /// TID is 13 chars, first 53 bits are microseconds since epoch 110 + /// parse TID to get timestamp in milliseconds 111 111 pub fn parseTidTimestamp(rkey: []const u8) i64 { 112 - if (rkey.len != 13) return 0; 113 - 114 - const charset = "234567abcdefghijklmnopqrstuvwxyz"; 115 - var value: u64 = 0; 116 - for (rkey) |c| { 117 - const idx = std.mem.indexOf(u8, charset, &[_]u8{c}) orelse return 0; 118 - value = value * 32 + idx; 119 - } 120 - 121 - // top 53 bits are microseconds, convert to milliseconds 122 - const micros = value >> 10; 123 - return @intCast(micros / 1000); 112 + const tid = zat.internal.Tid.parse(rkey) orelse return 0; 113 + return @intCast(tid.timestamp() / 1000); // microseconds -> milliseconds 124 114 } 125 115 126 116 /// parse TID from AT URI to get post timestamp 127 117 fn parseTimestampFromUri(uri: []const u8) ?i64 { 128 - const last_slash = std.mem.lastIndexOf(u8, uri, "/") orelse return null; 129 - const rkey = uri[last_slash + 1 ..]; 118 + const at_uri = zat.internal.AtUri.parse(uri) orelse return null; 119 + const rkey = at_uri.rkey() orelse return null; 130 120 const ts = parseTidTimestamp(rkey); 131 121 return if (ts > 0) ts else null; 132 122 } ··· 278 268 279 269 /// extract DID from post URI (at://did:plc:xxx/app.bsky.feed.post/yyy) 280 270 fn extractDidFromUri(uri: []const u8) ?[]const u8 { 281 - if (!std.mem.startsWith(u8, uri, "at://")) return null; 282 - const rest = uri[5..]; 283 - const end = std.mem.indexOf(u8, rest, "/") orelse return null; 284 - return rest[0..end]; 271 + const at_uri = zat.internal.AtUri.parse(uri) orelse return null; 272 + return at_uri.authority(); 285 273 } 286 274 287 275 /// get follows as newline-separated string (caller owns returned memory)