╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │11 const Self = @This(); │ │12 │ │13 pub const name_max_len = 32; │ │14 + │ │15 +// the moment keys `evt.merge` reads and writes for this kind │ │16 +pub const record_map_key = "event-id->repo"; │ │17 +pub const id_set_key = "repo-id-set"; │ │18 +pub const name_index_key = "name->repo-id"; │ │19 +pub const conflicts_key = "conflicted-repo-id->conflict"; │ │20 +pub const id_to_field_to_oid_key = "repo-id->field->oid"; │ │21 │ │22 +// a repo's key in the name index. it's unique per owner, so the read side │ │23 +// resolves the url's username to its user id first. │ │24 +pub fn indexKey(self: Self, allocator: std.mem.Allocator) ![]const u8 { │ │25 + return std.fmt.allocPrint(allocator, "{s}/{s}", .{ self.user_id, self.name });│ │26 +} │ │27 + │ │28 pub fn validateName(name: []const u8) !void { │ │29 if (name.len == 0) return error.NameEmpty; │ │30 if (name.len > name_max_len) return error.NameTooLong; │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │56 event_maybe: ?@This(), │ │57 arena: *std.heap.ArenaAllocator, │ │58 created_ts: u64, │ │59 + // the commit this event came from, recorded against every field it │ │60 + // changes. null from `merge`, which sets oids and conflicts itself.│ │61 + event_oid: ?[]const u8, │ │62 ) !void { │ │63 const repo_key = hash.hashInt(hash_kind, event_id); │ │64 │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │67 │ │68 const name_to_repo_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "name->repo-id")); │ │69 const name_to_repo_id = try DB.HashMap(.read_write).init(name_to_repo_id_cursor); │ │70 + │ │71 + const conflicts_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, conflicts_key)); │ │72 + const conflicts = try DB.SortedMap(.read_write).init(conflicts_cursor); │ │73 + │ │74 + const repo_id_to_field_to_oid_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key));│ │75 + const repo_id_to_field_to_oid = try DB.HashMap(.read_write).init(repo_id_to_field_to_oid_cursor); │ │76 │ │77 if (event_maybe) |event| { │ │78 try validateName(event.name); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │79 │ │80 var event_to_write = event; │ │81 │ │60 - // the index is keyed by "user-id/repo-name": a repo is unique per │ │61 - // owner, and the read side resolves the url's username to its user id. │ │62 - const repo_path = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ event.user_id, event.name }); │ │82 + const repo_path = try event.indexKey(arena.allocator()); │ │83 │ │84 const user_id_to_repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repo-id-set"));│ │85 const user_id_to_repo_id_set = try DB.HashMap(.read_write).init(user_id_to_repo_id_set_cursor); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │86 │ │87 // if this event_id already maps to a repo under a different key (its │ │88 // name or owner changed), drop the stale index entries first │ │89 + var existing_event_maybe: ?@This() = null; │ │90 const existing_cursor_maybe = try event_id_to_repo.getCursor(repo_key); │ │91 if (existing_cursor_maybe) |existing_cursor| { │ │92 const existing_repo = try DB.HashMap(.read_only).init(existing_cursor);│ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 93 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_repo); │ │ 94 + existing_event_maybe = existing_event; │ │ 95 // updates preserve the original creation timestamp │ │ 96 event_to_write.created_ts = existing_event.created_ts; │ │ 75 - const existing_path = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ existing_event.user_id, existing_event.name });│ │ 97 + │ │ 98 + // any event settles the conflict, since resolving in the ui may │ │ 99 + // keep our own values and so change nothing to detect │ │100 + if (event_oid != null) { │ │101 + _ = try conflicts.remove(&evt.orderKeyDesc(existing_event.created_ts, event_id)); │ │102 + } │ │103 + const existing_path = try existing_event.indexKey(arena.allocator()); │ │104 if (!std.mem.eql(u8, existing_path, repo_path)) { │ │105 _ = try name_to_repo_id.remove(hash.hashInt(hash_kind, existing_path)); │ │106 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │107 if (!std.mem.eql(u8, existing_event.user_id, event.user_id)) { │ │108 const old_user_repos_cursor = try user_id_to_repo_id_set.putCursor(hash.hashInt(hash_kind, existing_event.user_id));│ │109 const old_user_repos = try DB.SortedSet(.read_write).init(old_user_repos_cursor); │ │ 82 - const order_key = evt.orderKey(existing_event.created_ts, event_id); │ │110 + const order_key = evt.orderKeyDesc(existing_event.created_ts, event_id); │ │111 _ = try old_user_repos.remove(&order_key); │ │112 } │ │113 } else { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │115 event_to_write.created_ts = created_ts; │ │116 } │ │117 │ │118 + try evt.writeOid(@This(), DB, hash_kind, repo_id_to_field_to_oid, repo_key, existing_event_maybe, event, event_oid);│ │119 + │ │120 const repo_cursor = try event_id_to_repo.putCursor(repo_key); │ │121 const repo = try DB.HashMap(.read_write).init(repo_cursor); │ │122 try evt.upsert(@This(), DB, hash_kind, repo, event_to_write); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │124 try name_to_repo_id.put(hash.hashInt(hash_kind, repo_path), .{ .bytes = event_id }); │ │125 │ │126 // first time we've seen this repo: add it to the ordered set the repos │ │ 97 - // view paginates through. the key (orderKey) embeds the event id, so the │ │ 98 - // set needs no value. │ │127 + // view paginates through. the key embeds the event id, so the set needs │ │128 + // no value. │ │129 if (existing_cursor_maybe == null) { │ │130 const repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "repo-id-set"));│ │131 const repo_id_set = try DB.SortedSet(.read_write).init(repo_id_set_cursor); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │102 - const order_key = evt.orderKey(event_to_write.created_ts, event_id); │ │132 + const order_key = evt.orderKeyDesc(event_to_write.created_ts, event_id);│ │133 try repo_id_set.put(&order_key); │ │134 } │ │135 │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯