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