cut noise from 0.15 notes after critical review

- arraylist: focus on ownership (toOwnedSlice vs deinit), not "remember allocator"
- io: keep core patterns + tls quirk, add http.Client, delete history lesson
- build: just the 0.15 change and hash trick, delete boilerplate
- comptime: generalizable patterns, not zql description
- concurrency: design decisions (atomics vs mutex), not std.Thread syntax

-182 lines, mostly redundant/obvious content

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

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

Changed files
+136 -318
languages
+6 -8
languages/ziglang/0.15/README.md
··· 1 1 # zig 0.15 2 2 3 - [release notes](https://ziglang.org/download/0.15.1/release-notes.html) · [migration guide](https://sngeth.com/zig/systems-programming/breaking-changes/2025/10/24/zig-0-15-migration-roadblocks/) 4 - 5 - major breaking release with i/o overhaul ("writergate") and build system changes. 3 + [release notes](https://ziglang.org/download/0.15.1/release-notes.html) 6 4 7 5 ## notes 8 6 9 - - [arraylist](./arraylist.md) - now requires allocator on every method call 10 - - [io](./io.md) - new reader/writer interfaces with explicit buffers 11 - - [build](./build.md) - module imports pattern with `createModule` 12 - - [comptime](./comptime.md) - type-returning functions, inline for, reflection 13 - - [concurrency](./concurrency.md) - thread pools, mutexes, atomics, backoff 7 + - [arraylist](./arraylist.md) - ownership patterns (toOwnedSlice vs deinit) 8 + - [io](./io.md) - explicit buffers, http client/server, tls quirk 9 + - [build](./build.md) - createModule + imports, hash trick 10 + - [comptime](./comptime.md) - type generation, tuple synthesis, validation 11 + - [concurrency](./concurrency.md) - atomics vs mutex, callback pattern
+24 -40
languages/ziglang/0.15/arraylist.md
··· 1 1 # arraylist 2 2 3 - in 0.15, `std.ArrayList` became "unmanaged" by default - it no longer stores the allocator internally. you pass the allocator to every method call. 4 - 5 - ## why this changed 6 - 7 - the [release notes](https://ziglang.org/download/0.15.1/release-notes.html) explain the reasoning: "having an extra field is more complicated than not having an extra field, so not having it is the null hypothesis." 8 - 9 - storing the allocator had downsides: 10 - - worse method signatures when dealing with capacity reservations 11 - - can't statically initialize (no allocator available at comptime) 12 - - extra memory cost, especially for nested containers (arraylist of arraylists) 3 + 0.15 made arraylist unmanaged - allocator passed to each method. the compiler catches missing allocators immediately, so that's not worth documenting. what matters is ownership. 13 4 14 - the supposed upside - "avoiding accidentally using the wrong allocator" - isn't worth it since the correct allocator is always nearby and misuse can be safety-checked. 5 + ## ownership patterns 15 6 16 - ## the pattern 7 + **build and discard** - most common. defer cleanup, use `.items` to borrow: 17 8 18 9 ```zig 19 - var buf: std.ArrayList(u8) = .{}; 10 + var buf: std.ArrayList(u8) = .empty; 20 11 defer buf.deinit(alloc); 21 12 22 - try buf.append(alloc, 'x'); 23 - try buf.appendSlice(alloc, "hello"); 24 - 25 - const w = buf.writer(alloc); 26 - try w.print("{d}", .{42}); 13 + try buf.print(alloc, "{s}: {d}", .{ name, value }); 14 + sendResponse(buf.items); // borrow the slice 27 15 ``` 28 16 29 - every mutating method takes allocator as the first argument. this is the part i keep forgetting - `append(alloc, item)` not `append(item)`. 30 - 31 - ## initialization 32 - 33 - you can now statically initialize because there's no allocator field: 17 + **build and return** - transfer ownership, no defer: 34 18 35 19 ```zig 36 - var buf: std.ArrayList(u8) = .{}; // zero-init, no allocator needed yet 20 + var buf: std.ArrayList(u8) = .empty; 21 + // no defer - caller owns the memory 22 + 23 + try buf.appendSlice(alloc, data); 24 + return buf.toOwnedSlice(alloc); 37 25 ``` 38 26 39 - vs the old managed pattern which required an allocator upfront. 27 + the difference: `.items` borrows (arraylist still owns the memory), `.toOwnedSlice()` transfers (caller must free). 40 28 41 - ## writer 29 + see: [dashboard.zig#L187](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/dashboard.zig#L187) for the return pattern 42 30 43 - the writer also needs the allocator: 31 + ## direct methods vs writer 32 + 33 + arraylist has `.print()` directly - you don't always need a writer: 44 34 45 35 ```zig 46 - const w = buf.writer(alloc); 47 - try w.writeAll("data"); 48 - try w.print("{s}: {d}", .{ name, value }); 36 + try buf.print(alloc, "{{\"count\":{d}}}", .{count}); 49 37 ``` 50 38 51 - see: [leaflet-search/backend/src/server.zig#L129](https://tangled.sh/@zzstoatzz.io/leaflet-search/tree/main/backend/src/server.zig#L129) 52 - 53 - ## managed still exists 54 - 55 - if you really want the old behavior: 39 + use `.writer(alloc)` when you need to pass to something expecting `std.Io.Writer`: 56 40 57 41 ```zig 58 - var buf = std.array_list.Managed(u8).init(alloc); 59 - defer buf.deinit(); 60 - 61 - try buf.append('x'); // no allocator argument 42 + const w = buf.writer(alloc); 43 + try json.stringify(value, .{}, w); 62 44 ``` 63 45 64 - but this is marked for eventual removal. get used to passing the allocator. 46 + ## why unmanaged 47 + 48 + from the [release notes](https://ziglang.org/download/0.15.1/release-notes.html): storing the allocator had costs - worse method signatures for reservations, can't statically initialize, extra memory for nested containers. the benefits (convenience, avoiding wrong allocator) didn't justify it since the allocator is always nearby.
+8 -72
languages/ziglang/0.15/build.md
··· 1 - # build system 1 + # build 2 2 3 - 0.15 changed how you declare module dependencies in build.zig. the new pattern uses `createModule` with an `imports` array. 3 + ## 0.15 change 4 4 5 - ## executable with dependencies 5 + pre-0.15 used `exe.addModule()`. now use `createModule` with `imports` array: 6 6 7 7 ```zig 8 - const websocket = b.dependency("websocket", .{ 9 - .target = target, 10 - .optimize = optimize, 11 - }); 12 - 13 8 const exe = b.addExecutable(.{ 14 9 .name = "myapp", 15 10 .root_module = b.createModule(.{ ··· 23 18 }); 24 19 ``` 25 20 26 - the key difference from pre-0.15: 27 - - use `root_module` with `createModule()` instead of just `root_source_file` 28 - - dependencies go in the `imports` array, not via `addModule()` calls 29 - 30 - see: [music-atmosphere-feed/build.zig](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/build.zig) 31 - 32 - ## library module 33 - 34 - for a library that exposes a module to consumers: 21 + ## dependency hash trick 35 22 36 - ```zig 37 - const mod = b.addModule("zql", .{ 38 - .root_source_file = b.path("src/root.zig"), 39 - .target = target, 40 - .optimize = optimize, 41 - }); 23 + to get the hash for build.zig.zon, run `zig build` with a wrong hash. it tells you the correct one: 42 24 43 - const tests = b.addTest(.{ .root_module = mod }); 44 25 ``` 45 - 46 - the module name ("zql") is what consumers use in their `imports` array. 47 - 48 - see: [zql/build.zig](https://tangled.sh/@zzstoatzz.io/zql/tree/main/build.zig) 49 - 50 - ## build.zig.zon 51 - 52 - dependencies are declared in build.zig.zon: 53 - 54 - ```zig 55 - .{ 56 - .name = .my_project, 57 - .version = "0.0.1", 58 - .minimum_zig_version = "0.15.0", 59 - .dependencies = .{ 60 - .websocket = .{ 61 - .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.tar.gz", 62 - .hash = "...", 63 - }, 64 - }, 65 - } 66 - ``` 67 - 68 - to get the hash, run `zig build` with a wrong hash - it'll tell you the correct one. 69 - 70 - ## linking system libraries 71 - 72 - for sqlite, link both libc and the system library: 73 - 74 - ```zig 75 - exe.linkLibC(); 76 - exe.linkSystemLibrary("sqlite3"); 26 + error: hash mismatch... expected 1220abc..., found 1220def... 77 27 ``` 78 28 79 - see: [music-atmosphere-feed/build.zig#L30-31](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/build.zig#L30) 80 - 81 - ## run step 82 - 83 - ```zig 84 - const run_cmd = b.addRunArtifact(exe); 85 - run_cmd.step.dependOn(b.getInstallStep()); 86 - 87 - if (b.args) |args| { 88 - run_cmd.addArgs(args); 89 - } 90 - 91 - const run_step = b.step("run", "Run the server"); 92 - run_step.dependOn(&run_cmd.step); 93 - ``` 29 + ## don't forget 94 30 95 - this lets you do `zig build run -- --port 8080`. 31 + `b.installArtifact(exe)` - without this, `zig build` produces nothing.
+38 -69
languages/ziglang/0.15/comptime.md
··· 1 1 # comptime 2 2 3 - zig's comptime is where it gets interesting. you can generate types, validate inputs, and unroll loops at compile time. 3 + comptime lets you generate types, validate inputs, and catch errors at compile time. for a complete example, see [zql](https://tangled.sh/@zzstoatzz.io/zql) which parses SQL at comptime and generates type-safe bindings. 4 4 5 5 ## type-returning functions 6 6 7 - the pattern: a function that takes comptime parameters and returns a `type`. the returned type is a struct with fields and methods derived from the input. 7 + a function that takes comptime params and returns a `type`: 8 8 9 9 ```zig 10 - pub fn Query(comptime sql: []const u8) type { 11 - comptime { 12 - const parsed = parser.parse(sql); 13 - return struct { 14 - pub const raw = sql; 15 - pub const params: []const []const u8 = parsed.params[0..parsed.params_len]; 16 - pub const columns: []const []const u8 = parsed.columns[0..parsed.columns_len]; 10 + pub fn Wrapper(comptime T: type) type { 11 + return struct { 12 + value: T, 17 13 18 - pub fn bind(args: anytype) BindTuple(@TypeOf(args)) { 19 - // ... 20 - } 21 - }; 22 - } 14 + pub fn get(self: @This()) T { 15 + return self.value; 16 + } 17 + }; 23 18 } 24 19 ``` 25 20 26 - usage: 27 - 28 - ```zig 29 - const Q = Query("SELECT id, name FROM users WHERE age > :min_age"); 30 - // Q.params = ["min_age"] 31 - // Q.columns = ["id", "name"] 32 - ``` 21 + `@This()` refers to the struct being defined - necessary since the struct is anonymous. 33 22 34 - the entire SQL is parsed at compile time. no runtime overhead, and typos in parameter names are compile errors. 23 + ## generating tuple types from struct fields 35 24 36 - see: [zql/src/Query.zig](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig) 25 + extract field types in a specific order to build a tuple: 37 26 38 - ## inline for 27 + ```zig 28 + fn BindTuple(comptime Args: type, comptime param_names: []const []const u8) type { 29 + const fields = @typeInfo(Args).@"struct".fields; 30 + var types: [param_names.len]type = undefined; 39 31 40 - `inline for` unrolls the loop at compile time. each iteration becomes separate code: 41 - 42 - ```zig 43 - inline for (params) |p| { 44 - if (!hasField(fields, p)) { 45 - @compileError("missing param :" ++ p); 32 + inline for (param_names, 0..) |name, i| { 33 + for (fields) |f| { 34 + if (std.mem.eql(u8, f.name, name)) { 35 + types[i] = f.type; 36 + break; 37 + } 38 + } 46 39 } 40 + return std.meta.Tuple(&types); 47 41 } 48 42 ``` 49 43 50 - this checks at compile time that your args struct has all required SQL parameters. missing one? compile error with the exact parameter name. 51 - 52 - see: [zql/src/Query.zig#L23](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L23) 44 + this reorders struct fields into a tuple matching the parameter order. useful for binding named args to positional parameters. 53 45 54 - ## reflection 55 - 56 - access struct fields at comptime with `@typeInfo`: 57 - 58 - ```zig 59 - const fields = @typeInfo(Args).@"struct".fields; 60 - inline for (fields) |f| { 61 - @field(result, f.name) = @field(args, f.name); 62 - } 63 - ``` 64 - 65 - note the `@"struct"` syntax - struct is a keyword, so it needs quoting. 66 - 67 - this pattern maps an arbitrary struct's fields to another structure, checking types and names at compile time. 68 - 69 - see: [zql/src/Query.zig#L55](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L55) 46 + see: [zql/src/Query.zig#L78](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/Query.zig#L78) 70 47 71 48 ## compile-time validation 72 49 73 50 `@compileError` stops compilation with a message: 74 51 75 52 ```zig 76 - if (!hasColumn(f.name)) { 77 - @compileError("struct field '" ++ f.name ++ "' not found in query columns"); 53 + inline for (required_fields) |name| { 54 + if (!hasField(T, name)) { 55 + @compileError("missing required field: " ++ name); 56 + } 78 57 } 79 58 ``` 80 59 81 - this is how you make invalid states unrepresentable - if your code compiles, your query parameters match your struct fields. 60 + if your code compiles, it's valid. invalid states are unrepresentable. 82 61 83 62 ## branch quota 84 63 85 - complex comptime parsing can hit the default branch quota (1000 backwards branches). increase it: 64 + complex comptime parsing hits the default branch quota (1000 backwards branches). scale it with input: 86 65 87 66 ```zig 88 - @setEvalBranchQuota(sql.len * 100); 67 + @setEvalBranchQuota(input.len * 100); 89 68 ``` 90 69 91 - put this at the start of your comptime block. without it, complex SQL parsing fails with "evaluation exceeded maximum branch quota." 70 + without this, complex parsing fails with "evaluation exceeded maximum branch quota." 92 71 93 72 see: [zql/src/parse.zig#L48](https://tangled.sh/@zzstoatzz.io/zql/tree/main/src/parse.zig#L48) 94 73 95 - ## why this matters 96 - 97 - the zql library parses SQL at compile time and generates type-safe bindings: 98 - 99 - ```zig 100 - const Q = Query("INSERT INTO users (name, age) VALUES (:name, :age)"); 74 + ## constraints 101 75 102 - // this is checked at compile time: 103 - const args = Q.bind(.{ .name = "alice", .age = 25 }); 104 - // args is a tuple: ("alice", 25) in parameter order 105 - 106 - // if you typo .nme instead of .name, compile error 107 - ``` 108 - 109 - no runtime SQL parsing, no string concatenation, no injection vulnerabilities (because the SQL is a comptime string literal). 76 + - no allocation at comptime - use fixed-size arrays 77 + - no runtime values - everything must be known at compile time 78 + - comptime code runs during compilation, adding build time
+30 -69
languages/ziglang/0.15/concurrency.md
··· 1 1 # concurrency 2 2 3 - zig's concurrency primitives are in `std.Thread`. no async/await - just threads, mutexes, and atomics. 3 + zig has threads, mutexes, and atomics. no async/await. for syntax, see std.Thread docs. these notes cover design decisions. 4 4 5 - ## thread pool 5 + ## when to use atomics vs mutex 6 6 7 - for handling concurrent connections without spawning unbounded threads: 7 + **atomics for simple counters:** 8 8 9 9 ```zig 10 - var pool: Thread.Pool = undefined; 11 - try pool.init(.{ 12 - .allocator = allocator, 13 - .n_jobs = 16, 14 - }); 15 - defer pool.deinit(); 10 + posts_checked: std.atomic.Value(u64) = .init(0), 16 11 17 - // in accept loop: 18 - pool.spawn(handleConnection, .{conn}) catch |err| { 19 - conn.stream.close(); // cleanup on spawn failure 20 - }; 12 + _ = self.posts_checked.fetchAdd(1, .monotonic); 21 13 ``` 22 14 23 - the pool maintains a fixed number of worker threads. `spawn` queues work; if the pool is busy, it blocks or fails depending on configuration. 24 - 25 - see: [music-atmosphere-feed/src/main.zig#L29](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L29) 26 - 27 - ## background threads 28 - 29 - for long-running tasks like stream consumers: 15 + **mutex for complex data structures:** 30 16 31 17 ```zig 32 - const thread = try Thread.spawn(.{}, consumer, .{allocator}); 33 - defer thread.join(); // wait for completion on shutdown 18 + bufo_matches: std.StringHashMap(MatchInfo), 19 + bufo_mutex: Thread.Mutex = .{}, 20 + 21 + self.bufo_mutex.lock(); 22 + defer self.bufo_mutex.unlock(); 23 + try self.bufo_matches.put(name, info); 34 24 ``` 35 25 36 - the third argument is a tuple of arguments passed to the function. `join()` blocks until the thread exits. 26 + the pattern in [find-bufo/bot/src/stats.zig](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/stats.zig): atomics for the five simple counters (posts_checked, matches_found, etc.), mutex for the hashmap of per-bufo match data. 37 27 38 - see: [music-atmosphere-feed/src/main.zig#L25](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/main.zig#L25) 28 + rule: if it's a single integer, use atomic. if it's a container or multi-field update, use mutex. 39 29 40 - ## mutex 30 + ## memory ordering 41 31 42 - for protecting shared state: 32 + all usages in these projects use `.monotonic` - sufficient for independent counters where you just need eventual visibility, not synchronization between threads. 43 33 44 - ```zig 45 - const State = struct { 46 - mutex: Thread.Mutex = .{}, 47 - data: SomeData, 48 - }; 34 + use stricter orderings (`.acquire`, `.release`) when one thread's write must be visible to another thread before proceeding. none of these projects need that. 49 35 50 - state.mutex.lock(); 51 - defer state.mutex.unlock(); 52 - // critical section - only one thread at a time 53 - ``` 36 + ## callback pattern 54 37 55 - always use `defer` for unlock. if you return early or error, the mutex still unlocks. 56 - 57 - see: [find-bufo/bot/src/main.zig#L102](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L102) 58 - 59 - ## atomics 60 - 61 - for lock-free counters and flags: 38 + jetstream doesn't use channels or message passing. it takes a function pointer: 62 39 63 40 ```zig 64 - const Stats = struct { 65 - messages: std.atomic.Value(u64) = .init(0), 66 - }; 41 + callback: *const fn (Post) void, 67 42 68 - // increment (returns previous value) 69 - _ = self.messages.fetchAdd(1, .monotonic); 70 - 71 - // read 72 - const count = self.messages.load(.monotonic); 73 - 74 - // write 75 - self.messages.store(42, .monotonic); 43 + self.callback(.{ 44 + .uri = uri, 45 + .text = text, 46 + }); 76 47 ``` 77 48 78 - `.monotonic` is the memory ordering - sufficient for simple counters. use stricter orderings (`.acquire`, `.release`, `.seq_cst`) when you need synchronization guarantees between threads. 49 + simpler than channels when you just need to notify one consumer. 79 50 80 - see: [music-atmosphere-feed/src/stats.zig#L12](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/stats.zig#L12) 51 + see: [find-bufo/bot/src/jetstream.zig#L18](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/jetstream.zig#L18) 81 52 82 - ## exponential backoff 53 + ## reconnection 83 54 84 - for reconnection loops that don't hammer servers: 55 + exponential backoff for network consumers: 85 56 86 57 ```zig 87 58 var backoff: u64 = 1; 88 59 const max_backoff: u64 = 60; 89 60 90 61 while (true) { 91 - connect() catch |err| { 92 - std.debug.print("error: {}, retry in {}s\n", .{ err, backoff }); 93 - }; 62 + connect() catch {}; 94 63 posix.nanosleep(backoff, 0); 95 64 backoff = @min(backoff * 2, max_backoff); 96 65 } 97 66 ``` 98 67 99 - starts at 1 second, doubles each failure, caps at 60 seconds. simple and effective. 100 - 101 - see: [music-atmosphere-feed/src/jetstream.zig#L22](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/jetstream.zig#L22) 102 - 103 - ## no async 104 - 105 - zig removed async/await. the reasoning: it added complexity without clear benefits over threads for most use cases. if you need high concurrency, use a thread pool or io_uring (linux) / kqueue (macos). 106 - 107 - for bluesky bots and feed generators, a thread pool with 16 workers handles thousands of concurrent connections fine. 68 + starts at 1s, doubles each failure, caps at 60s.
+30 -60
languages/ziglang/0.15/io.md
··· 1 - # i/o interfaces 2 - 3 - 0.15 completely rewrote `std.io.Reader` and `std.io.Writer`. this was called "writergate" (referencing the earlier "allocgate" breaking change). the [release notes](https://ziglang.org/download/0.15.1/release-notes.html) acknowledge it as "extremely breaking" but necessary. 4 - 5 - ## what changed 6 - 7 - the old interfaces were generic (`anytype`), which forced every function that touched a reader/writer to also be generic. this "poisoned" APIs throughout codebases. 1 + # i/o 8 2 9 - the new interfaces are concrete types with explicit buffers. the buffer is part of the interface, not the implementation. 3 + 0.15 replaced generic `anytype` reader/writer with concrete types using explicit buffers. see [release notes](https://ziglang.org/download/0.15.1/release-notes.html) for the rationale. 10 4 11 - ## the pattern 12 - 13 - you now create explicit buffers and pass them to readers/writers: 5 + ## http server pattern 14 6 15 7 ```zig 16 8 var read_buffer: [8192]u8 = undefined; ··· 22 14 var server = http.Server.init(reader.interface(), &writer.interface); 23 15 ``` 24 16 25 - notice: 26 - - buffers are stack-allocated arrays you own 27 - - `.interface()` extracts the concrete interface type 28 - - http.Server no longer depends on `std.net` - it just takes interfaces 17 + the buffers are yours - stack allocated, explicit size. `.interface()` extracts the concrete type that http.Server expects. 29 18 30 - see: [music-atmosphere-feed/src/http.zig#L14-21](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/http.zig#L14) 19 + see: [http.zig#L14](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/http.zig#L14) 31 20 32 - ## why this matters 21 + ## http client pattern 33 22 34 - before 0.15, if you wrote a function that accepted a writer, you'd write: 35 - 36 - ```zig 37 - fn writeStuff(writer: anytype) !void { ... } 38 - ``` 39 - 40 - now you write: 41 - 42 - ```zig 43 - fn writeStuff(writer: *std.Io.Writer) !void { ... } 44 - ``` 45 - 46 - concrete types mean better error messages, actual type checking, and no generic explosion through your codebase. 47 - 48 - ## flushing 49 - 50 - with buffered i/o, you need to flush explicitly. the buffer accumulates writes and only sends when full or flushed: 23 + for api calls, use `Io.Writer.Allocating` to collect the response: 51 24 52 25 ```zig 53 - try writer.print("hello", .{}); 54 - try writer.flush(); // actually sends 55 - ``` 56 - 57 - forgetting to flush is a common bug - data sits in the buffer forever. 58 - 59 - ## allocating writer 60 - 61 - for dynamic output (like building json responses), use an allocating writer: 26 + var client = http.Client{ .allocator = allocator }; 27 + defer client.deinit(); 62 28 63 - ```zig 64 29 var aw: std.Io.Writer.Allocating = .init(allocator); 65 30 defer aw.deinit(); 66 31 67 32 const result = client.fetch(.{ 33 + .location = .{ .url = url }, 68 34 .response_writer = &aw.writer, 69 - ... 70 - }); 35 + }) catch |err| return err; 36 + 37 + if (result.status != .ok) return error.FetchFailed; 71 38 72 39 const response = aw.toArrayList().items; 73 40 ``` 74 41 75 - see: [find-bufo/bot/src/main.zig#L202-220](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L202) 76 - 77 - ## fixed writer 78 - 79 - for writing into a fixed-size buffer: 80 - 81 - ```zig 82 - var buf: [256]u8 = undefined; 83 - var w: std.Io.Writer = .fixed(&buf); 84 - try w.print("{s}: {d}", .{ name, value }); 85 - ``` 42 + see: [find-bufo/bot/src/main.zig#L196](https://tangled.sh/@zzstoatzz.io/find-bufo/tree/main/bot/src/main.zig#L196) 86 43 87 44 ## tls reading quirk 88 45 89 - when reading from tls streams, you must loop until data arrives. a return of 0 doesn't mean EOF - it means "try again": 46 + when reading from raw tls (not http.Client), you must loop until data arrives. `n == 0` means "try again", not EOF: 90 47 91 48 ```zig 92 49 outer: while (total_read < response_buf.len) { ··· 97 54 total_read += n; 98 55 break; 99 56 } 100 - // n == 0 means keep trying, not EOF 57 + // n == 0: tls may have consumed input without producing output 58 + // (buffering partial records, renegotiation, etc.) 101 59 } 102 60 } 103 61 ``` 104 62 105 - this pattern comes from [websocket.zig](https://github.com/karlseguin/websocket.zig). without the inner loop, you'll read 0 bytes and think you're done. 63 + this happens because tls decryption can consume input bytes without producing output yet. the inner loop keeps trying until actual data appears. 64 + 65 + also: raw tls needs explicit flush at both layers: 66 + ```zig 67 + tls_client.writer.flush() catch return error.Failed; 68 + stream_writer.interface.flush() catch return error.Failed; 69 + ``` 70 + 71 + see: [atproto.zig#L285](https://tangled.sh/@zzstoatzz.io/music-atmosphere-feed/tree/main/src/atproto.zig#L285) 72 + 73 + ## when you don't need to flush 74 + 75 + high-level apis (http.Server, http.Client) handle flushing internally. `request.respond()` flushes for you. only raw tls/stream code needs explicit flushes.