search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

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

consolidate module structure

- inline activity tracking into db/mod.zig
- inline domain types (Did, AtUri) into tap.zig
- remove activity.zig and types.zig

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

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

zzstoatzz 64209ad6 81e5fae0

+86 -184
-46
backend/src/activity.zig
··· 1 - const std = @import("std"); 2 - 3 - /// Ring buffer - 60 slots at 100ms each = 6 second window 4 - const SLOTS = 60; 5 - const TICK_MS = 100; 6 - 7 - var counts: [SLOTS]u16 = .{0} ** SLOTS; 8 - var current_slot: usize = 0; 9 - var mutex: std.Thread.Mutex = .{}; 10 - var tick_thread: ?std.Thread = null; 11 - 12 - /// Start the background tick thread 13 - pub fn init() void { 14 - tick_thread = std.Thread.spawn(.{}, tickLoop, .{}) catch null; 15 - } 16 - 17 - /// Record a search event 18 - pub fn record() void { 19 - mutex.lock(); 20 - defer mutex.unlock(); 21 - counts[current_slot] +|= 1; 22 - } 23 - 24 - /// Get activity counts (oldest to newest) 25 - pub fn getCounts() [SLOTS]u16 { 26 - mutex.lock(); 27 - defer mutex.unlock(); 28 - 29 - var result: [SLOTS]u16 = undefined; 30 - for (0..SLOTS) |i| { 31 - const idx = (current_slot + 1 + i) % SLOTS; 32 - result[i] = counts[idx]; 33 - } 34 - return result; 35 - } 36 - 37 - /// Background thread - advances slot every 100ms 38 - fn tickLoop() void { 39 - while (true) { 40 - std.Thread.sleep(TICK_MS * std.time.ns_per_ms); 41 - mutex.lock(); 42 - current_slot = (current_slot + 1) % SLOTS; 43 - counts[current_slot] = 0; 44 - mutex.unlock(); 45 - } 46 - }
+40 -2
backend/src/db/mod.zig
··· 6 6 const Client = @import("Client.zig"); 7 7 const schema = @import("schema.zig"); 8 8 const result = @import("result.zig"); 9 - const activity = @import("../activity.zig"); 9 + 10 + // activity tracking - ring buffer for real-time search activity 11 + const ACTIVITY_SLOTS = 60; 12 + const ACTIVITY_TICK_MS = 100; 13 + var activity_counts: [ACTIVITY_SLOTS]u16 = .{0} ** ACTIVITY_SLOTS; 14 + var activity_slot: usize = 0; 15 + var activity_mutex: std.Thread.Mutex = .{}; 16 + var activity_thread: ?std.Thread = null; 17 + 18 + fn activityTickLoop() void { 19 + while (true) { 20 + std.Thread.sleep(ACTIVITY_TICK_MS * std.time.ns_per_ms); 21 + activity_mutex.lock(); 22 + activity_slot = (activity_slot + 1) % ACTIVITY_SLOTS; 23 + activity_counts[activity_slot] = 0; 24 + activity_mutex.unlock(); 25 + } 26 + } 27 + 28 + pub fn initActivity() void { 29 + activity_thread = std.Thread.spawn(.{}, activityTickLoop, .{}) catch null; 30 + } 31 + 32 + pub fn getActivityCounts() [ACTIVITY_SLOTS]u16 { 33 + activity_mutex.lock(); 34 + defer activity_mutex.unlock(); 35 + var result_arr: [ACTIVITY_SLOTS]u16 = undefined; 36 + for (0..ACTIVITY_SLOTS) |i| { 37 + const idx = (activity_slot + 1 + i) % ACTIVITY_SLOTS; 38 + result_arr[i] = activity_counts[idx]; 39 + } 40 + return result_arr; 41 + } 42 + 43 + fn recordActivity() void { 44 + activity_mutex.lock(); 45 + defer activity_mutex.unlock(); 46 + activity_counts[activity_slot] +|= 1; 47 + } 10 48 11 49 pub const Row = result.Row; 12 50 pub const BatchResult = result.BatchResult; ··· 329 367 } 330 368 331 369 pub fn recordSearch(query: []const u8) void { 332 - activity.record(); // track for real-time sparkline 370 + recordActivity(); 333 371 var c = &(client orelse return); 334 372 c.exec("UPDATE stats SET total_searches = total_searches + 1 WHERE id = 1", &.{}) catch {}; 335 373
+1 -2
backend/src/main.zig
··· 5 5 const db = @import("db/mod.zig"); 6 6 const server = @import("server.zig"); 7 7 const tap = @import("tap.zig"); 8 - const activity = @import("activity.zig"); 9 8 10 9 const MAX_HTTP_WORKERS = 16; 11 10 const SOCKET_TIMEOUT_SECS = 30; ··· 19 18 try db.init(); 20 19 21 20 // start activity tracker 22 - activity.init(); 21 + db.initActivity(); 23 22 24 23 // start tap consumer in background 25 24 const tap_thread = try Thread.spawn(.{}, tap.consumer, .{allocator});
+1 -2
backend/src/server.zig
··· 4 4 const mem = std.mem; 5 5 const db = @import("db/mod.zig"); 6 6 const dashboard = @import("dashboard.zig"); 7 - const activity = @import("activity.zig"); 8 7 9 8 const HTTP_BUF_SIZE = 8192; 10 9 const QUERY_PARAM_BUF_SIZE = 64; ··· 218 217 } 219 218 220 219 fn handleActivity(request: *http.Server.Request) !void { 221 - const counts = activity.getCounts(); 220 + const counts = db.getActivityCounts(); 222 221 223 222 // format as JSON array 224 223 var buf: [512]u8 = undefined;
+44 -3
backend/src/tap.zig
··· 5 5 const Allocator = mem.Allocator; 6 6 const websocket = @import("websocket"); 7 7 const db = @import("db/mod.zig"); 8 - const types = @import("types.zig"); 9 - const Did = types.Did; 10 - const AtUri = types.AtUri; 11 8 12 9 const DOCUMENT_COLLECTION = "pub.leaflet.document"; 13 10 const PUBLICATION_COLLECTION = "pub.leaflet.publication"; 11 + 12 + // domain types 13 + const Did = struct { 14 + raw: []const u8, 15 + 16 + fn parse(s: []const u8) ?Did { 17 + if (!mem.startsWith(u8, s, "did:")) return null; 18 + const rest = s[4..]; 19 + const colon = mem.indexOf(u8, rest, ":") orelse return null; 20 + if (colon == 0 or colon == rest.len - 1) return null; 21 + return .{ .raw = s }; 22 + } 23 + 24 + fn str(self: Did) []const u8 { 25 + return self.raw; 26 + } 27 + }; 28 + 29 + const AtUri = struct { 30 + raw: []const u8, 31 + did_end: usize, 32 + collection_end: usize, 33 + 34 + fn build(allocator: Allocator, d: Did, coll: []const u8, rk: []const u8) !AtUri { 35 + const raw = try std.fmt.allocPrint(allocator, "at://{s}/{s}/{s}", .{ d.raw, coll, rk }); 36 + return .{ 37 + .raw = raw, 38 + .did_end = 5 + d.raw.len, 39 + .collection_end = 5 + d.raw.len + 1 + coll.len, 40 + }; 41 + } 42 + 43 + fn did(self: AtUri) Did { 44 + return .{ .raw = self.raw[5..self.did_end] }; 45 + } 46 + 47 + fn rkey(self: AtUri) []const u8 { 48 + return self.raw[self.collection_end + 1 ..]; 49 + } 50 + 51 + fn str(self: AtUri) []const u8 { 52 + return self.raw; 53 + } 54 + }; 14 55 15 56 fn getTapHost() []const u8 { 16 57 return posix.getenv("TAP_HOST") orelse "leaflet-search-tap.fly.dev";
-129
backend/src/types.zig
··· 1 - const std = @import("std"); 2 - const mem = std.mem; 3 - 4 - /// decentralized identifier - did:plc:xxx or did:web:xxx 5 - pub const Did = struct { 6 - raw: []const u8, 7 - 8 - pub fn parse(s: []const u8) ?Did { 9 - if (!mem.startsWith(u8, s, "did:")) return null; 10 - const rest = s[4..]; 11 - // must have method:identifier 12 - const colon = mem.indexOf(u8, rest, ":") orelse return null; 13 - if (colon == 0 or colon == rest.len - 1) return null; 14 - return .{ .raw = s }; 15 - } 16 - 17 - pub fn format(self: Did, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { 18 - try writer.writeAll(self.raw); 19 - } 20 - 21 - pub fn eql(self: Did, other: Did) bool { 22 - return mem.eql(u8, self.raw, other.raw); 23 - } 24 - 25 - pub fn str(self: Did) []const u8 { 26 - return self.raw; 27 - } 28 - }; 29 - 30 - /// at-uri - at://did/collection/rkey 31 - pub const AtUri = struct { 32 - raw: []const u8, 33 - did_end: usize, 34 - collection_end: usize, 35 - 36 - pub fn parse(s: []const u8) ?AtUri { 37 - if (!mem.startsWith(u8, s, "at://")) return null; 38 - const rest = s[5..]; 39 - 40 - // find did end (first slash after did) 41 - const did_end = mem.indexOf(u8, rest, "/") orelse return null; 42 - if (did_end == 0) return null; 43 - 44 - // validate did portion 45 - const did_str = rest[0..did_end]; 46 - if (Did.parse(did_str) == null) return null; 47 - 48 - // find collection end (second slash) 49 - const after_did = rest[did_end + 1 ..]; 50 - const collection_end = mem.indexOf(u8, after_did, "/") orelse return null; 51 - if (collection_end == 0) return null; 52 - 53 - // rkey must exist 54 - const rkey_part = after_did[collection_end + 1 ..]; 55 - if (rkey_part.len == 0) return null; 56 - 57 - return .{ 58 - .raw = s, 59 - .did_end = 5 + did_end, 60 - .collection_end = 5 + did_end + 1 + collection_end, 61 - }; 62 - } 63 - 64 - pub fn build(allocator: mem.Allocator, d: Did, coll: []const u8, rk: []const u8) !AtUri { 65 - const raw = try std.fmt.allocPrint(allocator, "at://{s}/{s}/{s}", .{ d.raw, coll, rk }); 66 - return .{ 67 - .raw = raw, 68 - .did_end = 5 + d.raw.len, 69 - .collection_end = 5 + d.raw.len + 1 + coll.len, 70 - }; 71 - } 72 - 73 - pub fn did(self: AtUri) Did { 74 - return .{ .raw = self.raw[5..self.did_end] }; 75 - } 76 - 77 - pub fn collection(self: AtUri) []const u8 { 78 - return self.raw[self.did_end + 1 .. self.collection_end]; 79 - } 80 - 81 - pub fn rkey(self: AtUri) []const u8 { 82 - return self.raw[self.collection_end + 1 ..]; 83 - } 84 - 85 - pub fn format(self: AtUri, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { 86 - try writer.writeAll(self.raw); 87 - } 88 - 89 - pub fn str(self: AtUri) []const u8 { 90 - return self.raw; 91 - } 92 - }; 93 - 94 - test "Did.parse valid" { 95 - const d = Did.parse("did:plc:abc123").?; 96 - try std.testing.expectEqualStrings("did:plc:abc123", d.raw); 97 - } 98 - 99 - test "Did.parse invalid" { 100 - try std.testing.expect(Did.parse("notadid") == null); 101 - try std.testing.expect(Did.parse("did:") == null); 102 - try std.testing.expect(Did.parse("did:plc") == null); 103 - try std.testing.expect(Did.parse("did::abc") == null); 104 - } 105 - 106 - test "AtUri.parse valid" { 107 - const u = AtUri.parse("at://did:plc:abc/app.bsky.post/123").?; 108 - try std.testing.expectEqualStrings("did:plc:abc", u.did().raw); 109 - try std.testing.expectEqualStrings("app.bsky.post", u.collection()); 110 - try std.testing.expectEqualStrings("123", u.rkey()); 111 - } 112 - 113 - test "AtUri.parse invalid" { 114 - try std.testing.expect(AtUri.parse("https://example.com") == null); 115 - try std.testing.expect(AtUri.parse("at://") == null); 116 - try std.testing.expect(AtUri.parse("at://did:plc:abc") == null); 117 - try std.testing.expect(AtUri.parse("at://did:plc:abc/collection") == null); 118 - try std.testing.expect(AtUri.parse("at://did:plc:abc/collection/") == null); 119 - } 120 - 121 - test "AtUri.build" { 122 - const d = Did.parse("did:plc:xyz").?; 123 - const u = try AtUri.build(std.testing.allocator, d, "pub.leaflet.document", "abc"); 124 - defer std.testing.allocator.free(u.raw); 125 - try std.testing.expectEqualStrings("at://did:plc:xyz/pub.leaflet.document/abc", u.raw); 126 - try std.testing.expectEqualStrings("did:plc:xyz", u.did().raw); 127 - try std.testing.expectEqualStrings("pub.leaflet.document", u.collection()); 128 - try std.testing.expectEqualStrings("abc", u.rkey()); 129 - }