╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │63 attach, │ │64 }; │ │65 │ │66 -// every logical event's kind and creation-ordered id set │ │66 +// every logical event's kind and creation-ordered active/removed id sets│ │67 pub const event_index_key = "event-id->kind"; │ │68 -pub const event_id_set_key = "event-id-set"; │ │68 +pub const active_event_id_set_key = "active-event-id-set"; │ │69 +pub const removed_event_id_set_key = "removed-event-id-set"; │ │70 │ │70 -// a null payload deletes the record │ │71 +// a null payload removes the record │ │72 pub const Event = union(EventKind) { │ │73 user: ?User, │ │74 repo: ?Repo, │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │164 } │ │165 }; │ │166 │ │167 +// remove an event by emitting a null payload │ │168 +pub fn remove( │ │169 + comptime repo_kind: rp.RepoKind, │ │170 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │171 + io: std.Io, │ │172 + allocator: std.mem.Allocator, │ │173 + repo: *rp.Repo(repo_kind, repo_opts), │ │174 + id: *const [event_id_size]u8, │ │175 + kind: EventKind, │ │176 + author: CommitAuthor, │ │177 +) !void { │ │178 + const DB = EventDB(repo_opts.hash); │ │179 + var event_db_maybe: ?LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try LocalEventDB(repo_opts.hash).openReadOnly(io, allocator, repo.core.repo_dir) else null;│ │180 + defer if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │181 + const moment = (if (event_db_maybe) |*event_db| │ │182 + currentMomentFromDb(repo_opts.hash, event_db.db) │ │183 + else if (repo_kind == .git) │ │184 + return error.EventNotFound │ │185 + else │ │186 + currentMoment(repo_opts, repo)) catch return error.EventNotFound; │ │187 + const kind_map_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, event_index_key)) orelse return error.EventNotFound; │ │188 + const kind_map = try DB.HashMap(.read_only).init(kind_map_cursor); │ │189 + const stored_kind = (try readEventKind(repo_opts.hash, kind_map, id)) orelse return error.EventNotFound; │ │190 + if (stored_kind != kind) return error.EventNotFound; │ │191 + if (event_db_maybe) |*event_db| event_db.deinit(io, allocator); │ │192 + event_db_maybe = null; │ │193 + │ │194 + const event: Event = switch (kind) { │ │195 + .user => .{ .user = null }, │ │196 + .repo => .{ .repo = null }, │ │197 + .issue => .{ .issue = null }, │ │198 + .comment => .{ .comment = null }, │ │199 + .attach => .{ .attach = null }, │ │200 + }; │ │201 + try consume(repo_kind, repo_opts, io, allocator, repo, events_ref, &.{.{ │ │202 + .id = std.fmt.bytesToHex(id.*, .lower), │ │203 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │204 + .author = author, │ │205 + .event = event, │ │206 + }}); │ │207 +} │ │208 + │ │209 // commit `events` (if any) as JSON commit messages on `ref`, then consume the │ │210 // events on `ref` into the db the repo's views read: the repo's own db for a │ │211 // xit repo, or the standalone event db next to a git repo. for a xit repo the │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │992 // three-way merge of one kind's records from a merge parent. kinds that expose│ │993 // conflicts merge field by field; the others take one whole record. the result│ │994 // goes through the kind's `consume`, which re-derives the indexes from it. a │ │952 -// deletion carries over unless the other side changed the record, so a merge │ │995 +// removal carries over unless the other side changed the record, so a merge │ │996 // never fails. │ │997 pub fn merge( │ │998 comptime T: type, │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1081 merged = target_record; │ │1082 merged.event = mergeFields(T, if (baseline_record) |baseline| baseline.event else null, target_record.event, parent_record.event, &outcome);│ │1083 │ │1041 - // merge the tombstone separately from serialized fields. a deletion │ │1084 + // merge the removed state separately from serialized fields. removal │ │1085 // carries over an unchanged record, while an edit or restoration │ │1086 // keeps it active. │ │1044 - merged.deleted = if (target_record.deleted == parent_record.deleted) │ │1045 - target_record.deleted │ │1087 + merged.removed = if (target_record.removed == parent_record.removed) │ │1088 + target_record.removed │ │1089 else if (baseline_record) |baseline| blk: { │ │1047 - const active = if (!target_record.deleted) target_record else parent_record; │ │1048 - if (active.deleted != baseline.deleted) break :blk false; │ │1090 + const active = if (!target_record.removed) target_record else parent_record; │ │1091 + if (active.removed != baseline.removed) break :blk false; │ │1092 if (!fieldsEqual(T, active.event, baseline.event)) break :blk false; │ │1093 break :blk true; │ │1094 } else false; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │1096 .target_wins => { │ │1097 // an uncontested parent change carries over. if both sides │ │1098 // changed the record, the target wins as a whole, except that│ │1056 - // an edit or restoration wins over a deletion. │ │1099 + // an edit or restoration wins over a removal. │ │1100 const target_changed = if (baseline_slot) |slot| │ │1101 !target_record_cursor.slot().eql(slot) │ │1102 else │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │1103 true; │ │1104 merged = if (!target_changed) │ │1105 parent_record │ │1063 - else if (target_record.deleted != parent_record.deleted) │ │1064 - if (target_record.deleted) parent_record else target_record│ │1106 + else if (target_record.removed != parent_record.removed) │ │1107 + if (target_record.removed) parent_record else target_record│ │1108 else │ │1109 target_record; │ │1110 }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1114 // the index is unique, so a key the target gave to a different record │ │1115 // leaves this one out rather than stranding that one behind a name it │ │1116 // no longer owns │ │1074 - if (!merged.deleted and @hasDecl(T, "name_index_key")) { │ │1117 + if (!merged.removed and @hasDecl(T, "name_index_key")) { │ │1118 const index_key = try merged.indexKey(arena.allocator()); │ │1119 const name_index_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.name_index_key));│ │1120 const name_index = try DB.HashMap(.read_write).init(name_index_cursor); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1134 .target_wins => continue, │ │1135 } │ │1136 │ │1094 - // tombstones do not carry field conflicts │ │1095 - if (merged.deleted) continue; │ │1137 + // removed records do not carry field conflicts │ │1138 + if (merged.removed) continue; │ │1139 │ │1140 const conflicts_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, T.conflicts_key));│ │1141 const conflicts = try DB.SortedMap(.read_write).init(conflicts_cursor); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────╮ │1272 } │ │1273 │ │1274 // add or refresh one entry in the global logical-event index. updates and│ │1232 -// tombstones preserve created_ts, so the key never moves. │ │1275 +// removed records preserve created_ts, so the key never moves. │ │1276 pub fn indexEvent( │ │1277 comptime DB: type, │ │1278 comptime hash_kind: hash.HashKind, │ ╰────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1280 event_id: *const [event_id_size]u8, │ │1281 kind: EventKind, │ │1282 created_ts: u64, │ │1283 + removed: bool, │ │1284 ) !void { │ │1285 const kind_map_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, event_index_key));│ │1286 const kind_map = try DB.HashMap(.read_write).init(kind_map_cursor); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯