atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

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

at main 192 lines 5.4 kB view raw view rendered
1# archived: initial plan (out of date) 2 3This file is preserved for context/history. Current direction lives in `docs/roadmap.md`. 4 5# zat - zig atproto primitives 6 7low-level building blocks for atproto applications in zig. not a full sdk - just the pieces that everyone reimplements. 8 9## philosophy 10 11from studying the wishlists: the pain is real, but the suggested solutions often over-engineer. we want: 12 131. **primitives, not frameworks** - types and parsers, not http clients or feed scaffolds 142. **layered design** - each piece usable independently 153. **zig idioms** - explicit buffers, comptime validation, no hidden allocations 164. **minimal scope** - solve the repeated pain, not every possible need 17 18## scope 19 20### in scope (v0.1) 21 22**tid** - timestamp identifiers 23- parse tid string to timestamp (microseconds) 24- generate tid from timestamp 25- extract clock id 26- comptime validation of format 27 28**at-uri** - `at://did:plc:xyz/collection/rkey` 29- parse to components (did, collection, rkey) 30- construct from components 31- validation 32 33**did** - decentralized identifiers 34- parse did:plc and did:web 35- validate format 36- type-safe wrapper (not just `[]const u8`) 37 38### maybe v0.2 39 40**facets** - extract links/mentions/tags from post records 41- given a json value with `text` and `facets`, extract urls 42- byte-offset handling for utf-8 43 44**cid** - content identifiers 45- parse cid strings 46- validate format 47 48### out of scope (for now) 49 50- lexicon codegen (too big, could be its own project) 51- xrpc client (std.http.Client is fine) 52- session management (app-specific) 53- jetstream client (websocket.zig exists, just wire it) 54- feed generator framework (each feed is unique) 55- did resolution (requires http, out of primitive scope) 56 57## design 58 59### tid.zig 60 61```zig 62pub const Tid = struct { 63 raw: [13]u8, 64 65 /// parse a tid string. returns null if invalid. 66 pub fn parse(s: []const u8) ?Tid 67 68 /// timestamp in microseconds since unix epoch 69 pub fn timestamp(self: Tid) u64 70 71 /// clock identifier (lower 10 bits) 72 pub fn clockId(self: Tid) u10 73 74 /// generate tid for current time 75 pub fn now() Tid 76 77 /// generate tid for specific timestamp 78 pub fn fromTimestamp(ts: u64, clock_id: u10) Tid 79 80 /// format to string 81 pub fn format(self: Tid, buf: *[13]u8) void 82}; 83``` 84 85encoding: base32-sortable (chars `234567abcdefghijklmnopqrstuvwxyz`), 13 chars, first 11 encode 53-bit timestamp, last 2 encode 10-bit clock id. 86 87### at_uri.zig 88 89```zig 90pub const AtUri = struct { 91 /// the full uri string (borrowed, not owned) 92 raw: []const u8, 93 94 /// offsets into raw for each component 95 did_end: usize, 96 collection_end: usize, 97 98 pub fn parse(s: []const u8) ?AtUri 99 100 pub fn did(self: AtUri) []const u8 101 pub fn collection(self: AtUri) []const u8 102 pub fn rkey(self: AtUri) []const u8 103 104 /// construct a new uri. caller owns the buffer. 105 pub fn format( 106 buf: []u8, 107 did: []const u8, 108 collection: []const u8, 109 rkey: []const u8, 110 ) ?[]const u8 111}; 112``` 113 114### did.zig 115 116```zig 117pub const Did = union(enum) { 118 plc: [24]u8, // the identifier after "did:plc:" 119 web: []const u8, // the domain after "did:web:" 120 121 pub fn parse(s: []const u8) ?Did 122 123 /// format to string 124 pub fn format(self: Did, buf: []u8) ?[]const u8 125 126 /// check if this is a plc did 127 pub fn isPlc(self: Did) bool 128}; 129``` 130 131## structure 132 133``` 134zat/ 135├── build.zig 136├── build.zig.zon 137├── src/ 138│ ├── root.zig # public API (stable exports) 139│ ├── internal.zig # internal API (experimental) 140│ └── internal/ 141│ ├── tid.zig 142│ ├── at_uri.zig 143│ └── did.zig 144└── docs/ 145 └── plan.md 146``` 147 148## internal → public promotion 149 150new features start in `internal` where we can iterate freely. when an API stabilizes: 151 152```zig 153// in root.zig, uncomment to promote: 154pub const Tid = internal.Tid; 155``` 156 157users who need bleeding-edge access can always use: 158 159```zig 160const zat = @import("zat"); 161const tid = zat.internal.Tid.parse("..."); 162``` 163 164this pattern exists indefinitely - even after 1.0, new experimental features start in internal. 165 166## decisions 167 168### why not typed lexicons? 169 170codegen from lexicon json is a big project on its own. the core pain (json navigation) can be partially addressed by documenting patterns, and the sdk should work regardless of how people parse json. 171 172### why not an http client wrapper? 173 174zig 0.15's `std.http.Client` with `Io.Writer.Allocating` works well. wrapping it doesn't add much value. the real pain is around auth token refresh and rate limiting - those are better solved at the application level where retry logic is domain-specific. 175 176### why not websocket/jetstream? 177 178websocket.zig already exists and works well. the jetstream protocol is simple json messages. a thin wrapper doesn't justify a dependency. 179 180### borrowing vs owning 181 182for parse operations, we borrow slices into the input rather than allocating. callers who need owned data can dupe. this matches zig's explicit memory style. 183 184## next steps 185 1861. ~~implement tid.zig with tests~~ done 1872. ~~implement at_uri.zig with tests~~ done 1883. ~~implement did.zig with tests~~ done 1894. ~~wire up build.zig as a module~~ done 1905. try using it in find-bufo or music-atmosphere-feed to validate the api 1916. iterate on internal APIs based on real usage 1927. promote stable APIs to root.zig