╭ 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, // comma-separated 9 status: Status = .open, 10 created_ts: u64 = 0, // the commit timestamp of the event that first created this issue 11 12 pub const Status = enum { 13 open, 14 closed, 15 16 const longest_len = blk: { 17 var len: usize = 0; 18 for (@typeInfo(Status).@"enum".fields) |field| len = @max(len, field.name.len); 19 break :blk len; 20 }; 21 }; 22 23 pub const tag_max_len = 64; 24 25 // a "tag,status" key names a tag's per-status issue set 26 pub const TagStatusKey = [tag_max_len + 1 + Status.longest_len]u8; 27 28 pub fn tagStatusKey(buffer: *TagStatusKey, tag: []const u8, status: Status) ![]const u8 { 29 return std.fmt.bufPrint(buffer, "{s},{s}", .{ tag, @tagName(status) }); 30 } 31 32 pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { 33 return std.mem.splitScalar(u8, tags, ','); 34 } 35 36 // `created_ts` comes from the commit timestamp, not the event 37 // payload, so it must not appear in the JSON we output 38 pub fn jsonStringify(self: @This(), jw: anytype) !void { 39 try jw.beginObject(); 40 inline for (std.meta.fields(@This())) |field| { 41 if (comptime std.mem.eql(u8, field.name, "created_ts")) continue; 42 try jw.objectField(field.name); 43 try jw.write(@field(self, field.name)); 44 } 45 try jw.endObject(); 46 } 47 48 pub fn consume( 49 comptime DB: type, 50 comptime hash_kind: hash.HashKind, 51 haxy_moment: DB.HashMap(.read_write), 52 event_id: *const [evt.event_id_size]u8, 53 event_maybe: ?@This(), 54 arena: *std.heap.ArenaAllocator, 55 created_ts: u64, 56 ) !void { 57 const issue_key = hash.hashInt(hash_kind, event_id); 58 59 const event_id_to_issue_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "event-id->issue")); 60 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); 61 62 // the per-status sets the issues views list, ordered by creation time, 63 // keyed by status name 64 const status_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "status->issue-id-set")); 65 const status_to_issues = try DB.SortedMap(.read_write).init(status_to_issues_cursor); 66 67 // the per-tag, per-status sets, keyed "tag,status" 68 const tag_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "tag+status->issue-id-set")); 69 const tag_to_issues = try DB.SortedMap(.read_write).init(tag_to_issues_cursor); 70 71 if (event_maybe) |event| { 72 var event_to_write = event; 73 74 const existing_cursor_maybe = try event_id_to_issue.getCursor(issue_key); 75 if (existing_cursor_maybe) |existing_cursor| { 76 // updates preserve the original creation timestamp 77 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); 78 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue); 79 event_to_write.created_ts = existing_event.created_ts; 80 81 // drop the old status's and tags' entries; the current ones are re-added below 82 const order_key = evt.orderKey(existing_event.created_ts, event_id); 83 const status_set = try statusSet(DB, status_to_issues, existing_event.status); 84 _ = try status_set.remove(&order_key); 85 try removeFromTagSets(DB, tag_to_issues, existing_event.tags, existing_event.status, &order_key); 86 } else { 87 // first time we've seen this issue: stamp it with the commit timestamp 88 event_to_write.created_ts = created_ts; 89 } 90 91 const issue_cursor = try event_id_to_issue.putCursor(issue_key); 92 const issue = try DB.HashMap(.read_write).init(issue_cursor); 93 try evt.upsert(@This(), DB, hash_kind, issue, event_to_write); 94 95 const order_key = evt.orderKey(event_to_write.created_ts, event_id); 96 97 const status_set = try statusSet(DB, status_to_issues, event.status); 98 try status_set.put(&order_key); 99 100 var tag_iter = tagIterator(event.tags); 101 while (tag_iter.next()) |tag| { 102 if (tag.len == 0) continue; 103 if (tag.len > tag_max_len) return error.TagTooLong; 104 var key_buffer: TagStatusKey = undefined; 105 const tag_set_cursor = try tag_to_issues.putCursor(try tagStatusKey(&key_buffer, tag, event.status)); 106 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); 107 try tag_set.put(&order_key); 108 } 109 } else { 110 // drop it from the ordered sets using its recorded creation timestamp and status 111 if (try event_id_to_issue.getCursor(issue_key)) |existing_cursor| { 112 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); 113 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue); 114 const order_key = evt.orderKey(existing_event.created_ts, event_id); 115 116 const status_set = try statusSet(DB, status_to_issues, existing_event.status); 117 _ = try status_set.remove(&order_key); 118 119 try removeFromTagSets(DB, tag_to_issues, existing_event.tags, existing_event.status, &order_key); 120 } 121 if (!try event_id_to_issue.remove(issue_key)) return error.EventNotFound; 122 } 123 } 124 125 // `status`'s sorted set within `statuses`, keyed by status name 126 fn statusSet( 127 comptime DB: type, 128 statuses: DB.SortedMap(.read_write), 129 status: Status, 130 ) !DB.SortedSet(.read_write) { 131 const cursor = try statuses.putCursor(@tagName(status)); 132 return DB.SortedSet(.read_write).init(cursor); 133 } 134 135 // remove an issue's order key from its tags' `status` sets, pruning entries 136 // whose set becomes empty 137 fn removeFromTagSets( 138 comptime DB: type, 139 tag_to_issues: DB.SortedMap(.read_write), 140 tags: []const u8, 141 status: Status, 142 order_key: []const u8, 143 ) !void { 144 var tag_iter = tagIterator(tags); 145 while (tag_iter.next()) |tag| { 146 if (tag.len == 0) continue; 147 var key_buffer: TagStatusKey = undefined; 148 const key = try tagStatusKey(&key_buffer, tag, status); 149 const tag_set_cursor = try tag_to_issues.putCursor(key); 150 const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); 151 _ = try tag_set.remove(order_key); 152 if (0 == try tag_set.count()) _ = try tag_to_issues.remove(key); 153 } 154 } 155