atproto pds in zig pds.zat.dev
pds atproto
24

Configure Feed

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

deps: move onto canonical first-party websocket/httpz/zat graph

Replace Karl's httpz fork (karlseguin.tngl.sh/http.zig) with our own
zzstoatzz.io/http.zig, which targets the canonical websocket v0.1.8
WorkerState.init(allocator, io) signature, and pin websocket directly at
v0.1.8. Bump zat to its current main (also on v0.1.8). The whole graph
now resolves to a single websocket version — no more upstream-fork API
mismatch.

Includes the zat.firehose commit-event wiring this consolidation unblocks.

zig build && zig build test green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

zzstoatzz (Jun 23, 2026, 1:02 AM -0500) 159022a4 f8df2242

+157 -47
+67 -2
build.zig
··· 17 17 .target = target, 18 18 .optimize = optimize, 19 19 }); 20 + const metrics = b.dependency("metrics", .{ 21 + .target = target, 22 + .optimize = optimize, 23 + }); 24 + const websocket = b.dependency("websocket", .{ 25 + .target = target, 26 + .optimize = optimize, 27 + }); 28 + const zat_mod = b.addModule("zat", .{ 29 + .root_source_file = zat.path("src/root.zig"), 30 + .target = target, 31 + .optimize = optimize, 32 + .link_libc = true, 33 + .imports = &.{ 34 + .{ .name = "websocket", .module = websocket.module("websocket") }, 35 + }, 36 + }); 37 + addZatInteropFixtures(b, zat_mod); 38 + 20 39 const httpz = b.dependency("httpz", .{ 21 40 .target = target, 22 41 .optimize = optimize, 23 42 }); 43 + const httpz_mod = b.addModule("httpz", .{ 44 + .link_libc = true, 45 + .root_source_file = httpz.path("src/httpz.zig"), 46 + .target = target, 47 + .optimize = optimize, 48 + .imports = &.{ 49 + .{ .name = "metrics", .module = metrics.module("metrics") }, 50 + .{ .name = "websocket", .module = websocket.module("websocket") }, 51 + }, 52 + }); 53 + { 54 + const options = b.addOptions(); 55 + options.addOption(bool, "httpz_blocking", false); 56 + httpz_mod.addOptions("build", options); 57 + } 24 58 25 59 const mod = b.addModule("zds", .{ 26 60 .root_source_file = b.path("src/root.zig"), ··· 28 62 .optimize = optimize, 29 63 .imports = &.{ 30 64 .{ .name = "build_options", .module = buildOptions(b, version) }, 31 - .{ .name = "zat", .module = zat.module("zat") }, 65 + .{ .name = "zat", .module = zat_mod }, 32 66 .{ .name = "zqlite", .module = zqlite.module("zqlite") }, 33 67 .{ .name = "webauthn", .module = webauthn.module("webauthn") }, 34 - .{ .name = "httpz", .module = httpz.module("httpz") }, 68 + .{ .name = "httpz", .module = httpz_mod }, 35 69 }, 36 70 }); 37 71 ··· 99 133 options.addOption([]const u8, "version", version); 100 134 return options.createModule(); 101 135 } 136 + 137 + fn addZatInteropFixtures(b: *std.Build, mod: *std.Build.Module) void { 138 + if (b.lazyDependency("atproto-interop-tests", .{})) |interop| { 139 + const interop_files = .{ 140 + .{ "tid_syntax_valid", "syntax/tid_syntax_valid.txt" }, 141 + .{ "tid_syntax_invalid", "syntax/tid_syntax_invalid.txt" }, 142 + .{ "did_syntax_valid", "syntax/did_syntax_valid.txt" }, 143 + .{ "did_syntax_invalid", "syntax/did_syntax_invalid.txt" }, 144 + .{ "handle_syntax_valid", "syntax/handle_syntax_valid.txt" }, 145 + .{ "handle_syntax_invalid", "syntax/handle_syntax_invalid.txt" }, 146 + .{ "nsid_syntax_valid", "syntax/nsid_syntax_valid.txt" }, 147 + .{ "nsid_syntax_invalid", "syntax/nsid_syntax_invalid.txt" }, 148 + .{ "recordkey_syntax_valid", "syntax/recordkey_syntax_valid.txt" }, 149 + .{ "recordkey_syntax_invalid", "syntax/recordkey_syntax_invalid.txt" }, 150 + .{ "aturi_syntax_valid", "syntax/aturi_syntax_valid.txt" }, 151 + .{ "aturi_syntax_invalid", "syntax/aturi_syntax_invalid.txt" }, 152 + .{ "signature_fixtures", "crypto/signature-fixtures.json" }, 153 + .{ "w3c_didkey_K256", "crypto/w3c_didkey_K256.json" }, 154 + .{ "w3c_didkey_P256", "crypto/w3c_didkey_P256.json" }, 155 + .{ "data_model_fixtures", "data-model/data-model-fixtures.json" }, 156 + .{ "mst_key_heights", "mst/key_heights.json" }, 157 + .{ "common_prefix", "mst/common_prefix.json" }, 158 + .{ "commit_proofs", "firehose/commit-proof-fixtures.json" }, 159 + }; 160 + inline for (interop_files) |entry| { 161 + mod.addAnonymousImport(entry[0], .{ 162 + .root_source_file = interop.path(entry[1]), 163 + }); 164 + } 165 + } 166 + }
+17 -4
build.zig.zon
··· 5 5 .minimum_zig_version = "0.16.0", 6 6 .dependencies = .{ 7 7 .zat = .{ 8 - .url = "git+https://tangled.org/zat.dev/zat#v0.3.5", 9 - .hash = "zat-0.3.5-5PuC7opUCQDOLCSXwKHv2Cp8YcxTqycUlaRajKs-IHwg", 8 + .url = "https://tangled.org/zat.dev/zat/archive/bb93dc212bec8cc1bd0263714a1acc68ea6c4388.tar.gz", 9 + .hash = "zat-0.3.7-5PuC7glgCgCV8hqxGrXTSVFnRNGP3MQAPoDZXg2vGNX0", 10 10 }, 11 11 .zqlite = .{ 12 12 .url = "git+https://github.com/karlseguin/zqlite.zig?ref=master#05a88d6758753e1c63fdd45b211dde2057094b0c", ··· 17 17 .hash = "webauthn-0.0.1--JitMv-LAACZoE0TF1Y4poG3EUvlk8q_qbz8NbLMPXAI", 18 18 }, 19 19 .httpz = .{ 20 - .url = "git+https://tangled.org/karlseguin.tngl.sh/http.zig?ref=master#744c588608d2b13e6dba2013577b2e0928877f19", 21 - .hash = "httpz-0.0.0-PNVzrCzKCAAKOXAKsuPzDZYzstlou0FLBp3HlDoRgmUa", 20 + .url = "https://tangled.org/zzstoatzz.io/http.zig/archive/f30aafd61fb45b5404ce6250b9286411da2d9b1f.tar.gz", 21 + .hash = "httpz-0.0.0-PNVzrLbSCAAcsNqeJdROW-fmNekyV0SlQIi-7qUgjNN_", 22 + }, 23 + .websocket = .{ 24 + .url = "https://tangled.org/zzstoatzz.io/websocket.zig/archive/v0.1.8.tar.gz", 25 + .hash = "websocket-0.1.8-ZPISdZ1cBADCBRYrCAZ8ueTIEN2T9ns7U7rtDE-IAvkr", 26 + }, 27 + .metrics = .{ 28 + .url = "https://github.com/karlseguin/metrics.zig/archive/6de29b83a750a06c438d268543e0e3c3c1b309da.tar.gz", 29 + .hash = "metrics-0.0.0-W7G4eIegAQD4XxA9Co7Atbw59u_2zvxYf406AZuoAHPM", 30 + }, 31 + .@"atproto-interop-tests" = .{ 32 + .url = "https://github.com/bluesky-social/atproto-interop-tests/archive/35bb5638ab1e5ce71fb88a0c95953fc557ef1925.tar.gz", 33 + .hash = "N-V-__8AAIp5AQCe4JjGmPl8CplTkCis8PF1qvn7QX6GwDfu", 34 + .lazy = true, 22 35 }, 23 36 }, 24 37 .paths = .{
+73 -41
src/storage/store.zig
··· 4875 4875 .blocks = blocks.items, 4876 4876 }); 4877 4877 4878 - var op_values = try allocator.alloc(zat.cbor.Value, ops.len); 4878 + var op_values = try allocator.alloc(zat.firehose.CommitEventOp, ops.len); 4879 4879 var record_idx: usize = 0; 4880 4880 for (ops, 0..) |op, idx| switch (op) { 4881 4881 .create => |create_op| { 4882 4882 const record = records[record_idx]; 4883 4883 record_idx += 1; 4884 - op_values[idx] = try firehoseOp(allocator, "create", create_op.collection, record.rkey, record.cid, null); 4884 + op_values[idx] = try firehoseOp(allocator, .create, create_op.collection, record.rkey, record.cid, null); 4885 4885 }, 4886 4886 .update => |update_op| { 4887 4887 const record = records[record_idx]; 4888 4888 record_idx += 1; 4889 - op_values[idx] = try firehoseOp(allocator, "update", update_op.collection, update_op.rkey, record.cid, prev_record_cids[idx]); 4889 + op_values[idx] = try firehoseOp(allocator, .update, update_op.collection, update_op.rkey, record.cid, prev_record_cids[idx]); 4890 4890 }, 4891 4891 .delete => |delete_op| { 4892 - op_values[idx] = try firehoseOp(allocator, "delete", delete_op.collection, delete_op.rkey, null, prev_record_cids[idx]); 4892 + op_values[idx] = try firehoseOp(allocator, .delete, delete_op.collection, delete_op.rkey, null, prev_record_cids[idx]); 4893 4893 }, 4894 4894 }; 4895 4895 return commitEventFrameFromCar(allocator, seq, did, commit_cid, rev, since_rev, prev_data_raw, car_bytes, op_values); ··· 4983 4983 since_rev: ?[]const u8, 4984 4984 prev_data_raw: ?[]const u8, 4985 4985 car_bytes: []const u8, 4986 - ops: []const zat.cbor.Value, 4986 + ops: []const zat.firehose.CommitEventOp, 4987 4987 ) ![]const u8 { 4988 - const header = try zat.cbor.encodeAlloc(allocator, .{ .map = &.{ 4989 - .{ .key = "op", .value = .{ .unsigned = 1 } }, 4990 - .{ .key = "t", .value = .{ .text = "#commit" } }, 4991 - } }); 4992 4988 const commit_raw = try cidRawFromText(allocator, commit_cid); 4993 - const body = try zat.cbor.encodeAlloc(allocator, .{ .map = &.{ 4994 - .{ .key = "seq", .value = .{ .unsigned = seq } }, 4995 - .{ .key = "rebase", .value = .{ .boolean = false } }, 4996 - .{ .key = "tooBig", .value = .{ .boolean = false } }, 4997 - .{ .key = "repo", .value = .{ .text = did } }, 4998 - .{ .key = "commit", .value = .{ .cid = .{ .raw = commit_raw } } }, 4999 - .{ .key = "rev", .value = .{ .text = rev } }, 5000 - .{ .key = "since", .value = if (since_rev) |since| .{ .text = since } else .null }, 5001 - .{ .key = "blocks", .value = .{ .bytes = car_bytes } }, 5002 - .{ .key = "ops", .value = .{ .array = ops } }, 5003 - .{ .key = "blobs", .value = .{ .array = &.{} } }, 5004 - .{ .key = "prevData", .value = if (prev_data_raw) |raw| .{ .cid = .{ .raw = raw } } else .null }, 5005 - .{ .key = "time", .value = .{ .text = try nowIso(allocator) } }, 5006 - } }); 5007 - return joinFrame(allocator, header, body); 4989 + return zat.firehose.encodeCommitEvent(allocator, .{ 4990 + .seq = @intCast(seq), 4991 + .repo_did = did, 4992 + .commit_cid = .{ .raw = commit_raw }, 4993 + .rev = rev, 4994 + .since_rev = since_rev, 4995 + .prev_data = if (prev_data_raw) |raw| .{ .raw = raw } else null, 4996 + .blocks = car_bytes, 4997 + .ops = ops, 4998 + .time = try nowIso(allocator), 4999 + }); 5008 5000 } 5009 5001 5010 5002 fn eventHeader(allocator: std.mem.Allocator, tag: []const u8) ![]const u8 { ··· 5023 5015 5024 5016 fn firehoseOp( 5025 5017 allocator: std.mem.Allocator, 5026 - action: []const u8, 5018 + action: zat.CommitAction, 5027 5019 collection: []const u8, 5028 5020 rkey: []const u8, 5029 5021 cid: ?[]const u8, 5030 5022 prev: ?[]const u8, 5031 - ) !zat.cbor.Value { 5032 - const path = try repoPath(allocator, collection, rkey); 5033 - const entries = try allocator.alloc(zat.cbor.Value.MapEntry, if (prev == null) 3 else 4); 5034 - entries[0] = .{ .key = "action", .value = .{ .text = action } }; 5035 - entries[1] = .{ .key = "path", .value = .{ .text = path } }; 5036 - entries[2] = .{ .key = "cid", .value = if (cid) |c| .{ .cid = .{ .raw = try cidRawFromText(allocator, c) } } else .null }; 5037 - if (prev) |p| entries[3] = .{ .key = "prev", .value = .{ .cid = .{ .raw = try cidRawFromText(allocator, p) } } }; 5038 - return .{ .map = entries }; 5023 + ) !zat.firehose.CommitEventOp { 5024 + return .{ 5025 + .action = action, 5026 + .collection = collection, 5027 + .rkey = rkey, 5028 + .cid = if (cid) |c| .{ .raw = try cidRawFromText(allocator, c) } else null, 5029 + .prev = if (prev) |p| .{ .raw = try cidRawFromText(allocator, p) } else null, 5030 + }; 5039 5031 } 5040 5032 5041 5033 fn nowIso(allocator: std.mem.Allocator) ![]const u8 { ··· 6446 6438 try std.testing.expect(std.mem.indexOf(u8, row.?.text(4), "app.bsky.feed.post") != null); 6447 6439 } 6448 6440 6449 - test "commit event encoding owns returned firehose op entries" { 6441 + test "commit event encoding uses Zat firehose builder" { 6450 6442 const allocator = std.testing.allocator; 6451 6443 const cid = "bafyreifmpxapiafzeml4ns5nedutwbsnjw5obdganycdy6mhmb2mxth5aq"; 6444 + const rev = try atid.encode(1_700_000_123_456_789, 1); 6445 + const since = try atid.encode(1_700_000_123_456_000, 1); 6446 + const prev_data_raw = try cidRawFromText(allocator, cid); 6447 + defer allocator.free(prev_data_raw); 6452 6448 6453 - const ops = try allocator.alloc(zat.cbor.Value, 1); 6449 + const ops = try allocator.alloc(zat.firehose.CommitEventOp, 3); 6454 6450 defer allocator.free(ops); 6455 - ops[0] = try firehoseOp(allocator, "create", "sh.tangled.string", "zds-test", cid, null); 6456 - defer if (ops[0] == .map) allocator.free(ops[0].map); 6451 + ops[0] = try firehoseOp(allocator, .create, "sh.tangled.string", "zds-create", cid, null); 6452 + ops[1] = try firehoseOp(allocator, .update, "sh.tangled.string", "zds-update", cid, cid); 6453 + ops[2] = try firehoseOp(allocator, .delete, "sh.tangled.string", "zds-delete", null, cid); 6457 6454 6458 6455 const frame = try commitEventFrameFromCar( 6459 6456 allocator, 6460 6457 1, 6461 6458 "did:plc:b64lsctzqnzpv6vd4ry3qktw", 6462 6459 cid, 6463 - "3zzzzzzzzzzzz", 6464 - null, 6465 - null, 6460 + &rev, 6461 + &since, 6462 + prev_data_raw, 6466 6463 "not-a-real-car-for-this-encoding-test", 6467 6464 ops, 6468 6465 ); 6469 6466 defer allocator.free(frame); 6470 - try std.testing.expect(frame.len > 0); 6467 + 6468 + const decoded = try zat.firehose.decodeFrame(allocator, frame); 6469 + try std.testing.expectEqual(@as(i64, 1), decoded.commit.seq); 6470 + try std.testing.expectEqualStrings("did:plc:b64lsctzqnzpv6vd4ry3qktw", decoded.commit.repo); 6471 + try std.testing.expectEqualStrings(&rev, decoded.commit.rev); 6472 + try std.testing.expectEqualStrings(&since, decoded.commit.since.?); 6473 + try std.testing.expectEqualSlices(u8, prev_data_raw, decoded.commit.prev_data.?.raw); 6474 + try std.testing.expectEqual(@as(usize, 3), decoded.commit.ops.len); 6475 + try std.testing.expectEqualStrings("sh.tangled.string/zds-create", decoded.commit.ops[0].path); 6476 + try std.testing.expectEqualSlices(u8, ops[0].cid.?.raw, decoded.commit.ops[0].cid.?.raw); 6477 + try std.testing.expectEqualStrings("sh.tangled.string/zds-update", decoded.commit.ops[1].path); 6478 + try std.testing.expectEqualSlices(u8, ops[1].cid.?.raw, decoded.commit.ops[1].cid.?.raw); 6479 + try std.testing.expectEqualSlices(u8, ops[1].prev.?.raw, decoded.commit.ops[1].prev.?.raw); 6480 + try std.testing.expectEqualStrings("sh.tangled.string/zds-delete", decoded.commit.ops[2].path); 6481 + try std.testing.expect(decoded.commit.ops[2].cid == null); 6482 + try std.testing.expectEqualSlices(u8, ops[2].prev.?.raw, decoded.commit.ops[2].prev.?.raw); 6483 + } 6484 + 6485 + test "Zat firehose builder rejects commit CID since rev" { 6486 + const allocator = std.testing.allocator; 6487 + const cid = "bafyreifmpxapiafzeml4ns5nedutwbsnjw5obdganycdy6mhmb2mxth5aq"; 6488 + const commit_raw = try cidRawFromText(allocator, cid); 6489 + defer allocator.free(commit_raw); 6490 + const rev = try atid.encode(1_700_000_123_456_789, 1); 6491 + 6492 + try std.testing.expectError(error.InvalidSince, zat.firehose.encodeCommitEvent(allocator, .{ 6493 + .seq = 1, 6494 + .repo_did = "did:plc:b64lsctzqnzpv6vd4ry3qktw", 6495 + .commit_cid = .{ .raw = commit_raw }, 6496 + .rev = &rev, 6497 + .since_rev = cid, 6498 + .prev_data = null, 6499 + .blocks = "not-a-real-car-for-this-encoding-test", 6500 + .ops = &.{}, 6501 + .time = "2026-06-22T00:00:00.000Z", 6502 + })); 6471 6503 } 6472 6504 6473 6505 test "repo rev generation does not move behind current head" {