╭ Issue.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../event.zig"); 3 const xit = @import("xit"); 4 const hash = xit.hash; 5 6 title: []const u8, 7 description: []const u8, 8 tags: []const u8, // NUL-separated 9 created_ts: u64 = 0, // the commit timestamp of the event that first created this issue 10 11 pub const tag_max_len = 64; 12 13 pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { 14 return std.mem.splitScalar(u8, tags, 0); 15 } 16 17 // `created_ts` comes from the commit timestamp, not the event 18 // payload, so it must not appear in the JSON we output 19 pub fn jsonStringify(self: @This(), jw: anytype) !void { 20 try jw.beginObject(); 21 inline for (std.meta.fields(@This())) |field| { 22 if (comptime std.mem.eql(u8, field.name, "created_ts")) continue; 23 try jw.objectField(field.name); 24 try jw.write(@field(self, field.name)); 25 } 26 try jw.endObject(); 27 } 28 29 pub fn consume( 30 comptime DB: type, 31 comptime hash_kind: hash.HashKind, 32 haxy_moment: DB.HashMap(.read_write), 33 event_id: *const [evt.event_id_size]u8, 34 event_maybe: ?@This(), 35 arena: *std.heap.ArenaAllocator, 36 created_ts: u64, 37 ) !void { 38 const issue_key = hash.hashInt(hash_kind, event_id); 39 40 const event_id_to_issue_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "event-id->issue")); 41 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); 42 43 // maps each tag to the set of issues carrying it, so a tag page can seek 44 // its window the same way the unfiltered issue list does. 45 const tag_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "tag->issue-id-set")); 46 const tag_to_issues = try DB.SortedMap(.read_write).init(tag_to_issues_cursor); 47 48 if (event_maybe) |event| { 49 var event_to_write = event; 50 51 const existing_cursor_maybe = try event_id_to_issue.getCursor(issue_key); 52 if (existing_cursor_maybe) |existing_cursor| { 53 // updates preserve the original creation timestamp 54 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); 55 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue); 56 event_to_write.created_ts = existing_event.created_ts; 57 58 // drop the old tags' entries; the current tags are re-added below 59 const order_key = evt.orderKey(existing_event.created_ts, event_id); 60 try removeFromTagSets(DB, tag_to_issues, existing_event.tags, &order_key); 61 } else { 62 // first time we've seen this issue: stamp it with the commit timestamp 63 event_to_write.created_ts = created_ts; 64 } 65 66 const issue_cursor = try event_id_to_issue.putCursor(issue_key); 67 const issue = try DB.HashMap(.read_write).init(issue_cursor); 68 try evt.upsert(@This(), DB, hash_kind, issue, event_to_write); 69 70 const order_key = evt.orderKey(event_to_write.created_ts, event_id); 71 72 var tag_iter = tagIterator(event.tags); 73 while (tag_iter.next()) |tag| { 74 if (tag.len == 0) continue; 75 if (tag.len > tag_max_len) return error.TagTooLong; 76 const tag_set_cursor = try tag_to_issues.putCursor(tag); 77 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); 78 try tag_set.put(&order_key); 79 } 80 81 // first time we've seen this issue: add it to the set the issues view 82 // lists, ordered by creation time. the key (orderKey) embeds the event 83 // id, so the set needs no value. 84 if (existing_cursor_maybe == null) { 85 const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "issue-id-set")); 86 const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); 87 try issue_id_set.put(&order_key); 88 } 89 } else { 90 // drop it from the ordered sets using its recorded creation timestamp 91 if (try event_id_to_issue.getCursor(issue_key)) |existing_cursor| { 92 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); 93 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue); 94 const order_key = evt.orderKey(existing_event.created_ts, event_id); 95 96 const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "issue-id-set")); 97 const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); 98 _ = try issue_id_set.remove(&order_key); 99 100 try removeFromTagSets(DB, tag_to_issues, existing_event.tags, &order_key); 101 } 102 if (!try event_id_to_issue.remove(issue_key)) return error.EventNotFound; 103 } 104 } 105 106 // remove an issue's order key from each of its tags' sets, pruning tags whose 107 // set becomes empty. 108 fn removeFromTagSets( 109 comptime DB: type, 110 tag_to_issues: DB.SortedMap(.read_write), 111 tags: []const u8, 112 order_key: []const u8, 113 ) !void { 114 var tag_iter = tagIterator(tags); 115 while (tag_iter.next()) |tag| { 116 if (tag.len == 0) continue; 117 const tag_set_cursor = try tag_to_issues.putCursor(tag); 118 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); 119 _ = try tag_set.remove(order_key); 120 if (0 == try tag_set.count()) _ = try tag_to_issues.remove(tag); 121 } 122 } 123