╭ Comment.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 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 parent_id: [evt.event_id_size * 2]u8, 9 body: []const u8, 10 11 // what the db stores: the event's data plus the commit-derived fields 12 pub const Record = struct { 13 event: Self, 14 deleted: bool = false, 15 author_email: ?[]const u8 = null, 16 created_ts: u64 = 0, // the commit timestamp of the event that first created this comment 17 }; 18 19 const Self = @This(); 20 21 // the moment keys `evt.merge` reads and writes for this kind 22 pub const record_map_key = "event-id->comment"; 23 pub const id_set_key = "comment-id-set"; 24 pub const conflicts_key = "conflicted-comment-id->conflict"; 25 pub const id_to_field_to_oid_key = "comment-id->field->oid"; 26 27 // the indexes the thread reads 28 pub const thread_id_to_comment_id_set_key = "thread-id->comment-id-set"; 29 pub const parent_id_to_comment_id_set_key = "parent-id->comment-id-set"; 30 31 pub fn fieldsValid(body: []const u8) bool { 32 return body.len != 0; 33 } 34 35 pub fn consume( 36 comptime DB: type, 37 comptime hash_kind: hash.HashKind, 38 haxy_moment: DB.HashMap(.read_write), 39 event_id: *const [evt.event_id_size]u8, 40 record_maybe: ?Record, 41 arena: *std.heap.ArenaAllocator, 42 // the commit this event came from, recorded against every field it 43 // changes. null from `merge`, which sets oids and conflicts itself. 44 event_oid: ?[]const u8, 45 ) !void { 46 const comment_key = hash.hashInt(hash_kind, event_id); 47 48 const event_id_to_comment_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key)); 49 const event_id_to_comment = try DB.HashMap(.read_write).init(event_id_to_comment_cursor); 50 51 const thread_id_to_comment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, thread_id_to_comment_id_set_key)); 52 const thread_id_to_comment_id_set = try DB.HashMap(.read_write).init(thread_id_to_comment_id_set_cursor); 53 54 const parent_id_to_comment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, parent_id_to_comment_id_set_key)); 55 const parent_id_to_comment_id_set = try DB.HashMap(.read_write).init(parent_id_to_comment_id_set_cursor); 56 57 const conflicts_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, conflicts_key)); 58 const conflicts = try DB.SortedMap(.read_write).init(conflicts_cursor); 59 60 const comment_id_to_field_to_oid_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key)); 61 const comment_id_to_field_to_oid = try DB.HashMap(.read_write).init(comment_id_to_field_to_oid_cursor); 62 63 var existing_record_maybe: ?Record = null; 64 const existing_cursor_maybe = try event_id_to_comment.getCursor(comment_key); 65 if (existing_cursor_maybe) |existing_cursor| { 66 const existing_comment = try DB.HashMap(.read_only).init(existing_cursor); 67 existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, existing_comment); 68 } 69 70 var record_to_write = if (record_maybe) |record| 71 record 72 else blk: { 73 var record = existing_record_maybe orelse return error.EventNotFound; 74 record.deleted = true; 75 break :blk record; 76 }; 77 78 // references use the same lower-case hex form as event ids in json 79 var id_bytes: [evt.event_id_size]u8 = undefined; 80 _ = try std.fmt.hexToBytes(&id_bytes, &record_to_write.event.thread_id); 81 if (!std.mem.eql(u8, &record_to_write.event.thread_id, &std.fmt.bytesToHex(id_bytes, .lower))) return error.InvalidEventId; 82 _ = try std.fmt.hexToBytes(&id_bytes, &record_to_write.event.parent_id); 83 if (!std.mem.eql(u8, &record_to_write.event.parent_id, &std.fmt.bytesToHex(id_bytes, .lower))) return error.InvalidEventId; 84 85 if (existing_record_maybe) |existing_record| { 86 // updates preserve the original creation timestamp and author 87 record_to_write.created_ts = existing_record.created_ts; 88 record_to_write.author_email = existing_record.author_email; 89 90 // a comment cannot move between threads or positions in the thread 91 if (!std.mem.eql(u8, &existing_record.event.thread_id, &record_to_write.event.thread_id)) return error.ThreadChanged; 92 if (!std.mem.eql(u8, &existing_record.event.parent_id, &record_to_write.event.parent_id)) return error.ParentChanged; 93 94 // any event settles the conflict, since resolving in the ui may 95 // keep our own values and so change nothing to detect 96 if (event_oid != null or record_to_write.deleted) { 97 _ = try conflicts.remove(&evt.orderKeyDesc(existing_record.created_ts, event_id)); 98 } 99 } 100 101 try evt.writeOid(Self, DB, hash_kind, comment_id_to_field_to_oid, comment_key, if (existing_record_maybe) |existing| existing.event else null, record_to_write.event, event_oid); 102 103 const comment_cursor = try event_id_to_comment.putCursor(comment_key); 104 const comment = try DB.HashMap(.read_write).init(comment_cursor); 105 try evt.upsert(Record, DB, hash_kind, comment, record_to_write); 106 107 if (existing_cursor_maybe == null) { 108 const comment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key)); 109 const comment_id_set = try DB.SortedSet(.read_write).init(comment_id_set_cursor); 110 try comment_id_set.put(&evt.orderKeyDesc(record_to_write.created_ts, event_id)); 111 } 112 113 // tombstones stay in the thread so their replies remain connected 114 const thread_order_key = evt.orderKey(record_to_write.created_ts, event_id); 115 const thread_comments_cursor = try thread_id_to_comment_id_set.putCursor(hash.hashInt(hash_kind, &record_to_write.event.thread_id)); 116 const thread_comments = try DB.SortedSet(.read_write).init(thread_comments_cursor); 117 try thread_comments.put(&thread_order_key); 118 119 const replies_cursor = try parent_id_to_comment_id_set.putCursor(hash.hashInt(hash_kind, &record_to_write.event.parent_id)); 120 const replies = try DB.SortedSet(.read_write).init(replies_cursor); 121 try replies.put(&thread_order_key); 122 } 123 124 // create a comment and return its event id. `repo` must be writable. 125 pub fn create( 126 comptime repo_kind: rp.RepoKind, 127 comptime repo_opts: rp.RepoOpts(repo_kind), 128 io: std.Io, 129 allocator: std.mem.Allocator, 130 repo: *rp.Repo(repo_kind, repo_opts), 131 thread_id: *const [evt.event_id_size * 2]u8, 132 parent_id: *const [evt.event_id_size * 2]u8, 133 body: []const u8, 134 author_email: []const u8, 135 ) ![evt.event_id_size * 2]u8 { 136 if (!fieldsValid(body)) return error.InvalidFields; 137 138 var id_bytes: [evt.event_id_size]u8 = undefined; 139 io.random(&id_bytes); 140 const event_id = std.fmt.bytesToHex(id_bytes, .lower); 141 try evt.consume(repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 142 .id = event_id, 143 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 144 .author_email = author_email, 145 .event = .{ .comment = .{ 146 .thread_id = thread_id.*, 147 .parent_id = parent_id.*, 148 .body = body, 149 } }, 150 }}); 151 return event_id; 152 } 153 154 // read a comment by event id, or null if the id isn't a known comment. field 155 // byte slices are allocated in `arena`. 156 pub fn readById( 157 comptime DB: type, 158 comptime hash_kind: hash.HashKind, 159 haxy_moment: DB.HashMap(.read_only), 160 arena: *std.heap.ArenaAllocator, 161 comment_id: []const u8, 162 ) !?Record { 163 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 164 const records = try DB.HashMap(.read_only).init(records_cursor); 165 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, comment_id)) orelse return null; 166 const record_map = try DB.HashMap(.read_only).init(record_cursor); 167 return try evt.read(Record, DB, hash_kind, arena, record_map); 168 } 169