╭ 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 merge_policy: evt.MergePolicy = .target_wins; 23 pub const record_map_key = "event-id->comment"; 24 pub const id_set_key = "comment-id-set"; 25 26 // the indexes the thread reads 27 pub const thread_id_to_comment_id_set_key = "thread-id->comment-id-set"; 28 pub const parent_id_to_comment_id_set_key = "parent-id->comment-id-set"; 29 30 pub fn fieldsValid(body: []const u8) bool { 31 return body.len != 0; 32 } 33 34 pub fn consume( 35 comptime DB: type, 36 comptime hash_kind: hash.HashKind, 37 haxy_moment: DB.HashMap(.read_write), 38 event_id: *const [evt.event_id_size]u8, 39 record_maybe: ?Record, 40 arena: *std.heap.ArenaAllocator, 41 _: ?[]const u8, 42 ) !void { 43 const comment_key = hash.hashInt(hash_kind, event_id); 44 45 const event_id_to_comment_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key)); 46 const event_id_to_comment = try DB.HashMap(.read_write).init(event_id_to_comment_cursor); 47 48 const thread_id_to_comment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, thread_id_to_comment_id_set_key)); 49 const thread_id_to_comment_id_set = try DB.HashMap(.read_write).init(thread_id_to_comment_id_set_cursor); 50 51 const parent_id_to_comment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, parent_id_to_comment_id_set_key)); 52 const parent_id_to_comment_id_set = try DB.HashMap(.read_write).init(parent_id_to_comment_id_set_cursor); 53 54 var existing_record_maybe: ?Record = null; 55 const existing_cursor_maybe = try event_id_to_comment.getCursor(comment_key); 56 if (existing_cursor_maybe) |existing_cursor| { 57 const existing_comment = try DB.HashMap(.read_only).init(existing_cursor); 58 existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, existing_comment); 59 } 60 61 var record_to_write = if (record_maybe) |record| 62 record 63 else blk: { 64 var record = existing_record_maybe orelse return error.EventNotFound; 65 record.deleted = true; 66 break :blk record; 67 }; 68 69 // references use the same lower-case hex form as event ids in json 70 var id_bytes: [evt.event_id_size]u8 = undefined; 71 _ = try std.fmt.hexToBytes(&id_bytes, &record_to_write.event.thread_id); 72 if (!std.mem.eql(u8, &record_to_write.event.thread_id, &std.fmt.bytesToHex(id_bytes, .lower))) return error.InvalidEventId; 73 _ = try std.fmt.hexToBytes(&id_bytes, &record_to_write.event.parent_id); 74 if (!std.mem.eql(u8, &record_to_write.event.parent_id, &std.fmt.bytesToHex(id_bytes, .lower))) return error.InvalidEventId; 75 76 if (existing_record_maybe) |existing_record| { 77 // updates preserve the original creation timestamp and author 78 record_to_write.created_ts = existing_record.created_ts; 79 record_to_write.author_email = existing_record.author_email; 80 81 // a comment cannot move between threads or positions in the thread 82 if (!std.mem.eql(u8, &existing_record.event.thread_id, &record_to_write.event.thread_id)) return error.ThreadChanged; 83 if (!std.mem.eql(u8, &existing_record.event.parent_id, &record_to_write.event.parent_id)) return error.ParentChanged; 84 } 85 86 const comment_cursor = try event_id_to_comment.putCursor(comment_key); 87 const comment = try DB.HashMap(.read_write).init(comment_cursor); 88 try evt.upsert(Record, DB, hash_kind, comment, record_to_write); 89 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .comment, record_to_write.created_ts); 90 91 if (existing_cursor_maybe == null) { 92 const comment_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key)); 93 const comment_id_set = try DB.SortedSet(.read_write).init(comment_id_set_cursor); 94 try comment_id_set.put(&evt.orderKeyDesc(record_to_write.created_ts, event_id)); 95 } 96 97 // tombstones stay in the thread so their replies remain connected 98 const thread_order_key = evt.orderKey(record_to_write.created_ts, event_id); 99 const thread_comments_cursor = try thread_id_to_comment_id_set.putCursor(hash.hashInt(hash_kind, &record_to_write.event.thread_id)); 100 const thread_comments = try DB.SortedSet(.read_write).init(thread_comments_cursor); 101 try thread_comments.put(&thread_order_key); 102 103 const replies_cursor = try parent_id_to_comment_id_set.putCursor(hash.hashInt(hash_kind, &record_to_write.event.parent_id)); 104 const replies = try DB.SortedSet(.read_write).init(replies_cursor); 105 try replies.put(&thread_order_key); 106 } 107 108 // create a comment and return its event id. `repo` must be writable. 109 pub fn create( 110 comptime repo_kind: rp.RepoKind, 111 comptime repo_opts: rp.RepoOpts(repo_kind), 112 io: std.Io, 113 allocator: std.mem.Allocator, 114 repo: *rp.Repo(repo_kind, repo_opts), 115 thread_id: *const [evt.event_id_size * 2]u8, 116 parent_id: *const [evt.event_id_size * 2]u8, 117 body: []const u8, 118 author_email: []const u8, 119 ) ![evt.event_id_size * 2]u8 { 120 if (!fieldsValid(body)) return error.InvalidFields; 121 122 var id_bytes: [evt.event_id_size]u8 = undefined; 123 io.random(&id_bytes); 124 const event_id = std.fmt.bytesToHex(id_bytes, .lower); 125 try evt.consume(repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 126 .id = event_id, 127 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 128 .author_email = author_email, 129 .event = .{ .comment = .{ 130 .thread_id = thread_id.*, 131 .parent_id = parent_id.*, 132 .body = body, 133 } }, 134 }}); 135 return event_id; 136 } 137 138 // replace a comment's body while preserving its thread and parent. `repo` must 139 // be writable. 140 pub fn update( 141 comptime repo_kind: rp.RepoKind, 142 comptime repo_opts: rp.RepoOpts(repo_kind), 143 io: std.Io, 144 allocator: std.mem.Allocator, 145 repo: *rp.Repo(repo_kind, repo_opts), 146 thread_id: *const [evt.event_id_size * 2]u8, 147 comment_id: *const [evt.event_id_size]u8, 148 body: []const u8, 149 author_email: []const u8, 150 ) !void { 151 if (!fieldsValid(body)) return error.InvalidFields; 152 153 var arena = std.heap.ArenaAllocator.init(allocator); 154 defer arena.deinit(); 155 156 const DB = evt.EventDB(repo_opts.hash); 157 var event_db_maybe: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null; 158 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 159 const moment = (if (event_db_maybe) |*event_db| 160 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 161 else if (repo_kind == .git) 162 return error.NotFound 163 else 164 evt.currentMoment(repo_opts, repo)) catch return error.NotFound; 165 const comment = (try readById(DB, repo_opts.hash, moment, &arena, comment_id)) orelse return error.NotFound; 166 if (!std.mem.eql(u8, &comment.event.thread_id, thread_id)) return error.NotFound; 167 168 var updated = comment.event; 169 updated.body = body; 170 try evt.consume(repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 171 .id = std.fmt.bytesToHex(comment_id.*, .lower), 172 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 173 .author_email = author_email, 174 .event = .{ .comment = updated }, 175 }}); 176 } 177 178 // read a comment by event id, or null if the id isn't a known comment. field 179 // byte slices are allocated in `arena`. 180 pub fn readById( 181 comptime DB: type, 182 comptime hash_kind: hash.HashKind, 183 haxy_moment: DB.HashMap(.read_only), 184 arena: *std.heap.ArenaAllocator, 185 comment_id: []const u8, 186 ) !?Record { 187 const records_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, record_map_key)) orelse return null; 188 const records = try DB.HashMap(.read_only).init(records_cursor); 189 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, comment_id)) orelse return null; 190 const record_map = try DB.HashMap(.read_only).init(record_cursor); 191 return try evt.read(Record, DB, hash_kind, arena, record_map); 192 } 193