╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Assignment.zig ───────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
2 const evt = @import("../event.zig"); 3 const xit = @import("xit"); 4 const rp = xit.repo; 5 const hash = xit.hash; 6 7 thread_id: [evt.event_id_size * 2]u8, 8 email: []const u8, 9 10 // what the db stores: the event's data plus the commit-derived fields 11 pub const Record = struct { 12 event: Self, 13 removed: bool = false, 14 author_email: ?[]const u8 = null, 15 created_order: u64 = 0, 16 updated_order: u64 = 0, 17 }; 18 19 const Self = @This(); 20 21 // the moment keys `evt.merge` reads and writes for this kind 22 pub const merge_policy: evt.MergePolicy = .target_wins; 23 pub const record_map_key = "event-id->assignment"; 24 pub const all_id_set_key = "assignment-id-set"; 25 26 // the index a thread's view reads 27 pub const thread_id_to_assignment_id_set_key = "thread-id->assignment-id-set"; 28 // the issues each email is assigned to, keyed like the issue status sets 29 pub const assignee_to_issue_id_set_key = "assignee->issue-id-set"; 30 31 // the longest email an assignment holds 32 pub const email_max_len = 254; 33 34 // the id of the one assignment an email can hold on a thread 35 pub fn idOf(thread_id: *const [evt.event_id_size]u8, email: []const u8) [evt.event_id_size]u8 { 36 var hasher = std.crypto.hash.sha2.Sha256.init(.{}); 37 hasher.update(thread_id); 38 hasher.update(email); 39 var digest: [std.crypto.hash.sha2.Sha256.digest_length]u8 = undefined; 40 hasher.final(&digest); 41 return digest[0..evt.event_id_size].*; 42 } 43 44 fn emailValid(email: []const u8) bool { 45 if (email.len == 0 or email.len > email_max_len) return false; 46 for (email) |ch| { 47 if (std.ascii.isWhitespace(ch)) return false; 48 } 49 return true; 50 } 51 52 // only what the event says about itself is checked. an assignment naming a 53 // missing thread is inert. 54 fn validate(event_id: *const [evt.event_id_size]u8, record: Record) !void { 55 const thread_id = try evt.parseEventId(&record.event.thread_id); 56 if (!emailValid(record.event.email)) return error.InvalidEmail; 57 if (!std.mem.eql(u8, event_id, &idOf(&thread_id, record.event.email))) return error.InvalidAssignmentId; 58 } 59 60 pub fn consume( 61 comptime DB: type, 62 comptime hash_kind: hash.HashKind, 63 haxy_moment: DB.HashMap(.read_write), 64 event_id: *const [evt.event_id_size]u8, 65 record_maybe: ?Record, 66 arena: *std.heap.ArenaAllocator, 67 _: ?[]const u8, 68 ) !void { 69 const assignment_key = hash.hashInt(hash_kind, event_id); 70 const records = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key))); 71 const by_thread = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, thread_id_to_assignment_id_set_key))); 72 73 var existing_maybe: ?Record = null; 74 const existing_cursor_maybe = try records.getCursor(assignment_key); 75 if (existing_cursor_maybe) |cursor| { 76 existing_maybe = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(cursor)); 77 } 78 79 var record = record_maybe orelse try evt.removedRecord(Record, DB, hash_kind, haxy_moment.readOnly(), existing_maybe); 80 81 if (!record.removed) try validate(event_id, record); 82 83 if (existing_maybe) |existing| { 84 // updates preserve the original creation metadata 85 record.created_order = existing.created_order; 86 record.author_email = existing.author_email; 87 } 88 89 const assignment_cursor = try records.putCursor(assignment_key); 90 try evt.upsert(Record, DB, hash_kind, try DB.HashMap(.read_write).init(assignment_cursor), record); 91 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .assign, existing_maybe, record); 92 93 // the id set retains removed records so merges can carry removals 94 if (existing_cursor_maybe == null) { 95 const ids = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, all_id_set_key))); 96 try ids.put(&evt.orderKeyDesc(record.created_order, event_id)); 97 } 98 99 // the thread's set holds active assignments only, oldest first. the id 100 // fixes the thread, so an assignment never moves between sets. 101 const order_key = evt.orderKey(record.created_order, event_id); 102 const thread_assignments = try DB.SortedSet(.read_write).init(try by_thread.putCursor(hash.hashInt(hash_kind, &record.event.thread_id))); 103 if (record.removed) { 104 _ = try thread_assignments.remove(&order_key); 105 } else { 106 try thread_assignments.put(&order_key); 107 } 108 109 // only an issue is indexed by assignee 110 const thread_id = try evt.parseEventId(&record.event.thread_id); 111 const issue = (try evt.readRecordSubset(evt.Issue, struct { created_order: u64, removed: bool }, DB, hash_kind, haxy_moment.readOnly(), arena, &thread_id)) orelse return; 112 try indexIssue(DB, hash_kind, haxy_moment, record.event.email, &evt.orderKeyDesc(issue.created_order, &thread_id), !record.removed and !issue.removed); 113 } 114 115 // put or drop an issue's order key in `email`'s assignee set, pruning an emptied set 116 pub fn indexIssue( 117 comptime DB: type, 118 comptime hash_kind: hash.HashKind, 119 haxy_moment: DB.HashMap(.read_write), 120 email: []const u8, 121 order_key: []const u8, 122 active: bool, 123 ) !void { 124 const by_assignee = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, assignee_to_issue_id_set_key))); 125 const email_key = hash.hashInt(hash_kind, email); 126 if (active) { 127 const issues = try DB.SortedSet(.read_write).init(try by_assignee.putCursor(email_key)); 128 try issues.put(order_key); 129 return; 130 } 131 if (null == try by_assignee.getCursor(email_key)) return; 132 const issues = try DB.SortedSet(.read_write).init(try by_assignee.putCursor(email_key)); 133 _ = try issues.remove(order_key); 134 if (0 == try issues.count()) _ = try by_assignee.remove(email_key); 135 } 136 137 // assign `email` to a thread and return the event id. `repo` must be writable. 138 pub fn create( 139 host: evt.Host, 140 comptime repo_kind: rp.RepoKind, 141 comptime repo_opts: rp.RepoOpts(repo_kind), 142 io: std.Io, 143 allocator: std.mem.Allocator, 144 repo: *rp.Repo(repo_kind, repo_opts), 145 thread_kind: evt.EventKind, 146 thread_id: *const [evt.event_id_size * 2]u8, 147 email: []const u8, 148 author: evt.CommitAuthor, 149 ) ![evt.event_id_size * 2]u8 { 150 if (!emailValid(email)) return error.InvalidFields; 151 152 const thread_id_bytes = evt.parseEventId(thread_id) catch return error.NotFound; 153 if (!try evt.threadHolds(repo_kind, repo_opts, io, allocator, repo, thread_kind, &thread_id_bytes, null)) return error.NotFound; 154 155 const id_bytes = idOf(&thread_id_bytes, email); 156 { 157 var arena = std.heap.ArenaAllocator.init(allocator); 158 defer arena.deinit(); 159 if (try evt.readFromRepo(Self, repo_kind, repo_opts, io, allocator, &arena, repo, &id_bytes)) |existing| { 160 if (!existing.removed) return error.AlreadyAssigned; 161 } 162 } 163 164 const event_id = std.fmt.bytesToHex(id_bytes, .lower); 165 try evt.consume(host, .repo, repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 166 .id = event_id, 167 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 168 .author = author, 169 .event = .{ .assign = .{ .thread_id = thread_id.*, .email = email } }, 170 }}); 171 return event_id; 172 } 173 174 // read an assignment by event id, or null if the id isn't a known assignment 175 pub fn readById( 176 comptime DB: type, 177 comptime hash_kind: hash.HashKind, 178 haxy_moment: DB.HashMap(.read_only), 179 arena: *std.heap.ArenaAllocator, 180 assignment_id: []const u8, 181 ) !?Record { 182 return try evt.readRecordSubset(Self, Record, DB, hash_kind, haxy_moment, arena, assignment_id); 183 } 184 185 // the emails assigned to a thread, oldest first 186 pub fn load( 187 comptime hash_kind: hash.HashKind, 188 arena: *std.heap.ArenaAllocator, 189 haxy_moment: evt.EventDB(hash_kind).HashMap(.read_only), 190 thread_id: []const u8, 191 ) ![]const []const u8 { 192 const DB = evt.EventDB(hash_kind); 193 var emails: std.ArrayList([]const u8) = .empty; 194 const index_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, thread_id_to_assignment_id_set_key)) orelse return emails.items; 195 const index = try DB.HashMap(.read_only).init(index_cursor); 196 const set_cursor = try index.getCursor(hash.hashInt(hash_kind, thread_id)) orelse return emails.items; 197 const set = try DB.SortedSet(.read_only).init(set_cursor); 198 199 var iter = try set.iteratorFromIndex(0); 200 while (try iter.next()) |kv_cursor| { 201 const id_bytes = try evt.readOrderKeyId(DB, kv_cursor); 202 const record = (try evt.readRecordSubset(Self, struct { event: struct { email: []const u8 } }, DB, hash_kind, haxy_moment, arena, &id_bytes)) orelse continue; 203 try emails.append(arena.allocator(), record.event.email); 204 } 205 return emails.items; 206 } 207