╭ Discussion.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 title: []const u8, 8 description: []const u8, 9 tags: []const u8, // space-separated 10 11 pub const Record = struct { 12 event: Self, 13 removed: bool = false, 14 author_email: ?[]const u8 = null, 15 created_ts: u64 = 0, 16 }; 17 18 const Self = @This(); 19 20 pub const tag_max_len = 64; 21 22 pub const merge_policy: evt.MergePolicy = .target_wins; 23 pub const record_map_key = "event-id->discussion"; 24 pub const id_set_key = "discussion-id-set"; 25 pub const active_id_set_key = "active-discussion-id-set"; 26 pub const tag_to_id_set_key = "tag->discussion-id-set"; 27 28 pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { 29 return std.mem.splitScalar(u8, tags, ' '); 30 } 31 32 pub fn fieldsValid(title: []const u8, tags: []const u8) bool { 33 if (title.len == 0) return false; 34 var tag_iter = tagIterator(tags); 35 while (tag_iter.next()) |tag| { 36 if (tag.len > tag_max_len) return false; 37 } 38 return true; 39 } 40 41 pub fn consume( 42 comptime DB: type, 43 comptime hash_kind: hash.HashKind, 44 haxy_moment: DB.HashMap(.read_write), 45 event_id: *const [evt.event_id_size]u8, 46 record_maybe: ?Record, 47 arena: *std.heap.ArenaAllocator, 48 _: ?[]const u8, 49 ) !void { 50 const discussion_key = hash.hashInt(hash_kind, event_id); 51 const records = try DB.HashMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, record_map_key))); 52 const active = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, active_id_set_key))); 53 const tags = try DB.SortedMap(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, tag_to_id_set_key))); 54 55 var existing_record_maybe: ?Record = null; 56 const existing_cursor_maybe = try records.getCursor(discussion_key); 57 if (existing_cursor_maybe) |existing_cursor| { 58 existing_record_maybe = try evt.read(Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(existing_cursor)); 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.removed = true; 66 break :blk record; 67 }; 68 69 if (existing_record_maybe) |existing| { 70 record_to_write.created_ts = existing.created_ts; 71 record_to_write.author_email = existing.author_email; 72 if (!existing.removed) { 73 const order_key = evt.orderKeyDesc(existing.created_ts, event_id); 74 _ = try active.remove(&order_key); 75 try removeFromTagSets(DB, tags, existing.event.tags, &order_key); 76 } 77 } 78 79 const record = try DB.HashMap(.read_write).init(try records.putCursor(discussion_key)); 80 try evt.upsert(Record, DB, hash_kind, record, record_to_write); 81 try evt.indexEvent(DB, hash_kind, haxy_moment, event_id, .discuss, record_to_write.created_ts, record_to_write.removed); 82 83 const order_key = evt.orderKeyDesc(record_to_write.created_ts, event_id); 84 if (existing_cursor_maybe == null) { 85 const ids = try DB.SortedSet(.read_write).init(try haxy_moment.putCursor(hash.hashInt(hash_kind, id_set_key))); 86 try ids.put(&order_key); 87 } 88 89 if (!record_to_write.removed) { 90 try active.put(&order_key); 91 var tag_iter = tagIterator(record_to_write.event.tags); 92 while (tag_iter.next()) |tag| { 93 if (tag.len == 0 or tag.len > tag_max_len) continue; 94 const set = try DB.SortedSet(.read_write).init(try tags.putCursor(tag)); 95 try set.put(&order_key); 96 } 97 } 98 } 99 100 pub fn update( 101 comptime repo_kind: rp.RepoKind, 102 comptime repo_opts: rp.RepoOpts(repo_kind), 103 io: std.Io, 104 allocator: std.mem.Allocator, 105 repo: *rp.Repo(repo_kind, repo_opts), 106 id: *const [evt.event_id_size]u8, 107 title: []const u8, 108 tags: []const u8, 109 description: []const u8, 110 author: evt.CommitAuthor, 111 ) !void { 112 if (!fieldsValid(title, tags)) return error.InvalidFields; 113 114 var arena = std.heap.ArenaAllocator.init(allocator); 115 defer arena.deinit(); 116 const discussion = (try readById(repo_kind, repo_opts, io, allocator, &arena, repo, id)) orelse return error.NotFound; 117 if (discussion.removed) return error.NotFound; 118 119 try evt.consume(repo_kind, repo_opts, io, allocator, repo, evt.events_ref, &.{.{ 120 .id = std.fmt.bytesToHex(id.*, .lower), 121 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), 122 .author = author, 123 .event = .{ .discuss = .{ 124 .title = title, 125 .tags = tags, 126 .description = description, 127 } }, 128 }}); 129 } 130 131 fn removeFromTagSets(comptime DB: type, tags: DB.SortedMap(.read_write), names: []const u8, order_key: []const u8) !void { 132 var tag_iter = tagIterator(names); 133 while (tag_iter.next()) |tag| { 134 if (tag.len == 0 or tag.len > tag_max_len) continue; 135 const cursor = try tags.putCursor(tag); 136 const set = try DB.SortedSet(.read_write).init(cursor); 137 _ = try set.remove(order_key); 138 if (0 == try set.count()) _ = try tags.remove(tag); 139 } 140 } 141 142 pub fn readById( 143 comptime repo_kind: rp.RepoKind, 144 comptime repo_opts: rp.RepoOpts(repo_kind), 145 io: std.Io, 146 allocator: std.mem.Allocator, 147 arena: *std.heap.ArenaAllocator, 148 repo: *rp.Repo(repo_kind, repo_opts), 149 id: *const [evt.event_id_size]u8, 150 ) !?Record { 151 const DB = evt.EventDB(repo_opts.hash); 152 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; 153 defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); 154 const moment = (if (event_db_maybe) |*event_db| 155 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 156 else if (repo_kind == .git) 157 return null 158 else 159 evt.currentMoment(repo_opts, repo)) catch return null; 160 const records_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, record_map_key)) orelse return null; 161 const records = try DB.HashMap(.read_only).init(records_cursor); 162 const record_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, id)) orelse return null; 163 return try evt.read(Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(record_cursor)); 164 } 165