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.

refactor: reorganize backend - domain modules at src/, db/ for infrastructure

- move activity.zig out of db/ (not db-related)
- move search.zig to src/ (application logic)
- rename write.zig to indexer.zig at src/
- move stats.zig to src/ (application logic)
- slim db/mod.zig to 24 lines (just init + getClient)

db/ now purely infrastructure (Client, Row, schema)
src/ has domain-focused modules (search, indexer, stats, activity)

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

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

zzstoatzz 1394d094 4693f2ff

+57 -123
backend/src/db/activity.zig backend/src/activity.zig
+1 -87
backend/src/db/mod.zig
··· 1 1 const std = @import("std"); 2 - const Allocator = std.mem.Allocator; 3 2 4 3 const Client = @import("Client.zig"); 5 4 const schema = @import("schema.zig"); 6 5 const result = @import("result.zig"); 7 6 8 - // submodules 9 - pub const activity = @import("activity.zig"); 10 - const search_ = @import("search.zig"); 11 - const stats_ = @import("stats.zig"); 12 - const write_ = @import("write.zig"); 13 - 14 7 // re-exports 15 8 pub const Row = result.Row; 9 + pub const Result = result.Result; 16 10 pub const BatchResult = result.BatchResult; 17 - pub const Statement = Client.Statement; 18 - pub const Stats = stats_.Stats; 19 11 20 12 // global state 21 13 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{}; ··· 30 22 if (client) |*c| return c; 31 23 return null; 32 24 } 33 - 34 - // activity (direct re-export) 35 - pub const initActivity = activity.init; 36 - pub const getActivityCounts = activity.getCounts; 37 - 38 - // search 39 - pub fn search(alloc: Allocator, query: []const u8, tag_filter: ?[]const u8) ![]const u8 { 40 - const c = &(client orelse return error.NotInitialized); 41 - return search_.search(c, alloc, query, tag_filter); 42 - } 43 - 44 - pub fn findSimilar(alloc: Allocator, uri: []const u8, limit: usize) ![]const u8 { 45 - const c = &(client orelse return error.NotInitialized); 46 - return search_.findSimilar(c, alloc, uri, limit); 47 - } 48 - 49 - // stats 50 - pub fn getTags(alloc: Allocator) ![]const u8 { 51 - const c = &(client orelse return error.NotInitialized); 52 - return stats_.getTags(c, alloc); 53 - } 54 - 55 - pub fn getStats() Stats { 56 - const c = &(client orelse return .{ .documents = 0, .publications = 0, .searches = 0, .errors = 0, .started_at = 0 }); 57 - return stats_.getStats(c); 58 - } 59 - 60 - pub fn recordSearch(query: []const u8) void { 61 - const c = &(client orelse return); 62 - stats_.recordSearch(c, query); 63 - } 64 - 65 - pub fn recordError() void { 66 - const c = &(client orelse return); 67 - stats_.recordError(c); 68 - } 69 - 70 - pub fn getPopular(alloc: Allocator, limit: usize) ![]const u8 { 71 - const c = &(client orelse return error.NotInitialized); 72 - return stats_.getPopular(c, alloc, limit); 73 - } 74 - 75 - // write 76 - pub fn insertDocument( 77 - uri: []const u8, 78 - did: []const u8, 79 - rkey: []const u8, 80 - title: []const u8, 81 - content: []const u8, 82 - created_at: ?[]const u8, 83 - publication_uri: ?[]const u8, 84 - tags: []const []const u8, 85 - ) !void { 86 - const c = &(client orelse return error.NotInitialized); 87 - return write_.insertDocument(c, uri, did, rkey, title, content, created_at, publication_uri, tags); 88 - } 89 - 90 - pub fn insertPublication( 91 - uri: []const u8, 92 - did: []const u8, 93 - rkey: []const u8, 94 - name: []const u8, 95 - description: ?[]const u8, 96 - base_path: ?[]const u8, 97 - ) !void { 98 - const c = &(client orelse return error.NotInitialized); 99 - return write_.insertPublication(c, uri, did, rkey, name, description, base_path); 100 - } 101 - 102 - pub fn deleteDocument(uri: []const u8) void { 103 - const c = &(client orelse return); 104 - write_.deleteDocument(c, uri); 105 - } 106 - 107 - pub fn deletePublication(uri: []const u8) void { 108 - const c = &(client orelse return); 109 - write_.deletePublication(c, uri); 110 - }
+9 -7
backend/src/db/search.zig backend/src/search.zig
··· 2 2 const json = std.json; 3 3 const Allocator = std.mem.Allocator; 4 4 const zql = @import("zql"); 5 - const Client = @import("Client.zig"); 6 - const result = @import("result.zig"); 7 - const Row = result.Row; 5 + const db = @import("db/mod.zig"); 8 6 9 7 // JSON output type for search results 10 8 const SearchResultJson = struct { ··· 29 27 basePath: []const u8, 30 28 hasPublication: bool, 31 29 32 - fn fromRow(row: Row) Doc { 30 + fn fromRow(row: db.Row) Doc { 33 31 return .{ 34 32 .uri = row.text(0), 35 33 .did = row.text(1), ··· 101 99 rkey: []const u8, 102 100 basePath: []const u8, 103 101 104 - fn fromRow(row: Row) Pub { 102 + fn fromRow(row: db.Row) Pub { 105 103 return .{ 106 104 .uri = row.text(0), 107 105 .did = row.text(1), ··· 135 133 \\ORDER BY rank LIMIT 10 136 134 ); 137 135 138 - pub fn search(c: *Client, alloc: Allocator, query: []const u8, tag_filter: ?[]const u8) ![]const u8 { 136 + pub fn search(alloc: Allocator, query: []const u8, tag_filter: ?[]const u8) ![]const u8 { 137 + const c = db.getClient() orelse return error.NotInitialized; 138 + 139 139 var output: std.Io.Writer.Allocating = .init(alloc); 140 140 errdefer output.deinit(); 141 141 ··· 175 175 } 176 176 177 177 /// Find documents similar to a given document using vector similarity 178 - pub fn findSimilar(c: *Client, alloc: Allocator, uri: []const u8, limit: usize) ![]const u8 { 178 + pub fn findSimilar(alloc: Allocator, uri: []const u8, limit: usize) ![]const u8 { 179 + const c = db.getClient() orelse return error.NotInitialized; 180 + 179 181 var output: std.Io.Writer.Allocating = .init(alloc); 180 182 errdefer output.deinit(); 181 183
+15 -6
backend/src/db/stats.zig backend/src/stats.zig
··· 2 2 const json = std.json; 3 3 const Allocator = std.mem.Allocator; 4 4 const zql = @import("zql"); 5 - const Client = @import("Client.zig"); 5 + const db = @import("db/mod.zig"); 6 6 const activity = @import("activity.zig"); 7 7 8 8 const TagJson = struct { tag: []const u8, count: i64 }; ··· 16 16 \\LIMIT 100 17 17 ); 18 18 19 - pub fn getTags(c: *Client, alloc: Allocator) ![]const u8 { 19 + pub fn getTags(alloc: Allocator) ![]const u8 { 20 + const c = db.getClient() orelse return error.NotInitialized; 21 + 20 22 var output: std.Io.Writer.Allocating = .init(alloc); 21 23 errdefer output.deinit(); 22 24 ··· 41 43 started_at: i64, 42 44 }; 43 45 44 - pub fn getStats(c: *Client) Stats { 46 + pub fn getStats() Stats { 47 + const c = db.getClient() orelse return .{ .documents = 0, .publications = 0, .searches = 0, .errors = 0, .started_at = 0 }; 48 + 45 49 var res = c.query( 46 50 \\SELECT 47 51 \\ (SELECT COUNT(*) FROM documents) as docs, ··· 62 66 }; 63 67 } 64 68 65 - pub fn recordSearch(c: *Client, query: []const u8) void { 69 + pub fn recordSearch(query: []const u8) void { 70 + const c = db.getClient() orelse return; 71 + 66 72 activity.record(); 67 73 c.exec("UPDATE stats SET total_searches = total_searches + 1 WHERE id = 1", &.{}) catch {}; 68 74 ··· 75 81 } 76 82 } 77 83 78 - pub fn recordError(c: *Client) void { 84 + pub fn recordError() void { 85 + const c = db.getClient() orelse return; 79 86 c.exec("UPDATE stats SET total_errors = total_errors + 1 WHERE id = 1", &.{}) catch {}; 80 87 } 81 88 82 - pub fn getPopular(c: *Client, alloc: Allocator, limit: usize) ![]const u8 { 89 + pub fn getPopular(alloc: Allocator, limit: usize) ![]const u8 { 90 + const c = db.getClient() orelse return error.NotInitialized; 91 + 83 92 var output: std.Io.Writer.Allocating = .init(alloc); 84 93 errdefer output.deinit(); 85 94
+11 -5
backend/src/db/write.zig backend/src/indexer.zig
··· 1 1 const std = @import("std"); 2 - const Client = @import("Client.zig"); 2 + const db = @import("db/mod.zig"); 3 3 4 4 pub fn insertDocument( 5 - c: *Client, 6 5 uri: []const u8, 7 6 did: []const u8, 8 7 rkey: []const u8, ··· 12 11 publication_uri: ?[]const u8, 13 12 tags: []const []const u8, 14 13 ) !void { 14 + const c = db.getClient() orelse return error.NotInitialized; 15 + 15 16 try c.exec( 16 17 "INSERT OR REPLACE INTO documents (uri, did, rkey, title, content, created_at, publication_uri) VALUES (?, ?, ?, ?, ?, ?, ?)", 17 18 &.{ uri, did, rkey, title, content, created_at orelse "", publication_uri orelse "" }, ··· 35 36 } 36 37 37 38 pub fn insertPublication( 38 - c: *Client, 39 39 uri: []const u8, 40 40 did: []const u8, 41 41 rkey: []const u8, ··· 43 43 description: ?[]const u8, 44 44 base_path: ?[]const u8, 45 45 ) !void { 46 + const c = db.getClient() orelse return error.NotInitialized; 47 + 46 48 try c.exec( 47 49 "INSERT OR REPLACE INTO publications (uri, did, rkey, name, description, base_path) VALUES (?, ?, ?, ?, ?, ?)", 48 50 &.{ uri, did, rkey, name, description orelse "", base_path orelse "" }, ··· 56 58 ) catch {}; 57 59 } 58 60 59 - pub fn deleteDocument(c: *Client, uri: []const u8) void { 61 + pub fn deleteDocument(uri: []const u8) void { 62 + const c = db.getClient() orelse return; 63 + 60 64 // record tombstone 61 65 var ts_buf: [20]u8 = undefined; 62 66 const ts = std.fmt.bufPrint(&ts_buf, "{d}", .{std.time.timestamp()}) catch "0"; ··· 70 74 c.exec("DELETE FROM document_tags WHERE document_uri = ?", &.{uri}) catch {}; 71 75 } 72 76 73 - pub fn deletePublication(c: *Client, uri: []const u8) void { 77 + pub fn deletePublication(uri: []const u8) void { 78 + const c = db.getClient() orelse return; 79 + 74 80 // record tombstone 75 81 var ts_buf: [20]u8 = undefined; 76 82 const ts = std.fmt.bufPrint(&ts_buf, "{d}", .{std.time.timestamp()}) catch "0";
+2 -1
backend/src/main.zig
··· 3 3 const posix = std.posix; 4 4 const Thread = std.Thread; 5 5 const db = @import("db/mod.zig"); 6 + const activity = @import("activity.zig"); 6 7 const server = @import("server.zig"); 7 8 const tap = @import("tap.zig"); 8 9 ··· 18 19 try db.init(); 19 20 20 21 // start activity tracker 21 - db.initActivity(); 22 + activity.init(); 22 23 23 24 // start tap consumer in background 24 25 const tap_thread = try Thread.spawn(.{}, tap.consumer, .{allocator});
+14 -12
backend/src/server.zig
··· 2 2 const net = std.net; 3 3 const http = std.http; 4 4 const mem = std.mem; 5 - const db = @import("db/mod.zig"); 5 + const activity = @import("activity.zig"); 6 + const search = @import("search.zig"); 7 + const stats = @import("stats.zig"); 6 8 const dashboard = @import("dashboard.zig"); 7 9 8 10 const HTTP_BUF_SIZE = 8192; ··· 82 84 } 83 85 84 86 // perform FTS search - arena handles cleanup 85 - const results = db.search(alloc, query, tag_filter) catch |err| { 86 - db.recordError(); 87 + const results = search.search(alloc, query, tag_filter) catch |err| { 88 + stats.recordError(); 87 89 return err; 88 90 }; 89 - db.recordSearch(query); 91 + stats.recordSearch(query); 90 92 try sendJson(request, results); 91 93 } 92 94 ··· 95 97 defer arena.deinit(); 96 98 const alloc = arena.allocator(); 97 99 98 - const tags = try db.getTags(alloc); 100 + const tags = try stats.getTags(alloc); 99 101 try sendJson(request, tags); 100 102 } 101 103 ··· 104 106 defer arena.deinit(); 105 107 const alloc = arena.allocator(); 106 108 107 - const popular = try db.getPopular(alloc, 5); 109 + const popular = try stats.getPopular(alloc, 5); 108 110 try sendJson(request, popular); 109 111 } 110 112 ··· 113 115 const patterns = [_][]const u8{ "?", "&" }; 114 116 for (patterns) |prefix| { 115 117 var search_buf: [QUERY_PARAM_BUF_SIZE]u8 = undefined; 116 - const search = std.fmt.bufPrint(&search_buf, "{s}{s}=", .{ prefix, param }) catch continue; 117 - if (mem.indexOf(u8, target, search)) |idx| { 118 - const encoded = target[idx + search.len ..]; 118 + const search_str = std.fmt.bufPrint(&search_buf, "{s}{s}=", .{ prefix, param }) catch continue; 119 + if (mem.indexOf(u8, target, search_str)) |idx| { 120 + const encoded = target[idx + search_str.len ..]; 119 121 const end = mem.indexOf(u8, encoded, "&") orelse encoded.len; 120 122 const query_encoded = encoded[0..end]; 121 123 const buf = try alloc.dupe(u8, query_encoded); ··· 134 136 defer arena.deinit(); 135 137 const alloc = arena.allocator(); 136 138 137 - const db_stats = db.getStats(); 139 + const db_stats = stats.getStats(); 138 140 139 141 var response: std.ArrayList(u8) = .{}; 140 142 defer response.deinit(alloc); ··· 214 216 return; 215 217 }; 216 218 217 - const results = db.findSimilar(alloc, uri, 5) catch { 219 + const results = search.findSimilar(alloc, uri, 5) catch { 218 220 try sendJson(request, "[]"); 219 221 return; 220 222 }; ··· 223 225 } 224 226 225 227 fn handleActivity(request: *http.Server.Request) !void { 226 - const counts = db.getActivityCounts(); 228 + const counts = activity.getCounts(); 227 229 228 230 // format as JSON array 229 231 var buf: [512]u8 = undefined;
+5 -5
backend/src/tap.zig
··· 5 5 const Allocator = mem.Allocator; 6 6 const websocket = @import("websocket"); 7 7 const zat = @import("zat"); 8 - const db = @import("db/mod.zig"); 8 + const indexer = @import("indexer.zig"); 9 9 10 10 const DOCUMENT_COLLECTION = "pub.leaflet.document"; 11 11 const PUBLICATION_COLLECTION = "pub.leaflet.publication"; ··· 155 155 }, 156 156 .delete => { 157 157 if (mem.eql(u8, rec.collection, DOCUMENT_COLLECTION)) { 158 - db.deleteDocument(uri); 158 + indexer.deleteDocument(uri); 159 159 std.debug.print("deleted document: {s}\n", .{uri}); 160 160 } else if (mem.eql(u8, rec.collection, PUBLICATION_COLLECTION)) { 161 - db.deletePublication(uri); 161 + indexer.deletePublication(uri); 162 162 std.debug.print("deleted publication: {s}\n", .{uri}); 163 163 } 164 164 }, ··· 203 203 204 204 if (content_buf.items.len == 0) return; 205 205 206 - try db.insertDocument(uri, did, rkey, doc.title, content_buf.items, created_at, doc.publication, tags_list.items); 206 + try indexer.insertDocument(uri, did, rkey, doc.title, content_buf.items, created_at, doc.publication, tags_list.items); 207 207 std.debug.print("indexed document: {s} ({} chars, {} tags)\n", .{ uri, content_buf.items.len, tags_list.items.len }); 208 208 } 209 209 ··· 292 292 const record_val: json.Value = .{ .object = record }; 293 293 const pub_data = zat.json.extractAt(LeafletPublication, allocator, record_val, .{}) catch return; 294 294 295 - try db.insertPublication(uri, did, rkey, pub_data.name, pub_data.description, pub_data.base_path); 295 + try indexer.insertPublication(uri, did, rkey, pub_data.name, pub_data.description, pub_data.base_path); 296 296 std.debug.print("indexed publication: {s} (base_path: {s})\n", .{ uri, pub_data.base_path orelse "none" }); 297 297 }