╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │144 const existing_repo = try DB.HashMap(.read_only).init(existing_repo_cursor); │ │145 const existing_repo_event = try evt.read(@This(), DB, hash_kind, arena, existing_repo); │ │146 │ │117 - const existing_path = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ existing_repo_event.user_id, existing_repo_event.name });│ │147 + const existing_path = try existing_repo_event.indexKey(arena.allocator()); │ │148 _ = try name_to_repo_id.remove(hash.hashInt(hash_kind, existing_path)); │ │149 │ │150 // drop it from the ordered set using its recorded creation timestamp │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 const repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "repo-id-set")); │ │152 const repo_id_set = try DB.SortedSet(.read_write).init(repo_id_set_cursor); │ │123 - const order_key = evt.orderKey(existing_repo_event.created_ts, event_id); │ │153 + const order_key = evt.orderKeyDesc(existing_repo_event.created_ts, event_id); │ │154 _ = try repo_id_set.remove(&order_key); │ │155 │ │156 const user_id_to_repo_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repo-id-set"));│ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │160 const user_repos_cursor = try user_id_to_repo_id_set.putCursor(user_key);│ │161 const user_repos = try DB.SortedSet(.read_write).init(user_repos_cursor);│ │162 _ = try user_repos.remove(&order_key); │ │163 + │ │164 + _ = try conflicts.remove(&order_key); │ │165 + _ = try repo_id_to_field_to_oid.remove(repo_key); │ │166 } │ │167 if (!try event_id_to_repo.remove(repo_key)) return error.EventNotFound; │ │168 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │194 _ = try user_id_cursor.readBytes(&user_id); │ │195 │ │196 // "user-id/repo-name" -> repo event id │ │164 - const repo_path = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ user_id[0..], repo_name }); │ │197 + const repo_path = try (Self{ .user_id = user_id[0..], .name = repo_name, .description = "" }).indexKey(arena.allocator());│ │198 const name_to_repo_id_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "name->repo-id")) orelse return null; │ │199 const name_to_repo_id = try DB.HashMap(.read_only).init(name_to_repo_id_cursor); │ │200 const repo_id_cursor = try name_to_repo_id.getCursor(hash.hashInt(hash_kind, repo_path)) orelse return null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │16 const Self = @This(); │ │17 │ │18 pub const name_max_len = 32; │ │19 + │ │20 +// the moment keys `evt.merge` reads and writes for this kind │ │21 +pub const record_map_key = "event-id->user"; │ │22 +pub const id_set_key = "user-id-set"; │ │23 +pub const name_index_key = "name->user-id"; │ │24 +pub const conflicts_key = "conflicted-user-id->conflict"; │ │25 +pub const id_to_field_to_oid_key = "user-id->field->oid"; │ │26 + │ │27 +// a user's key in the name index │ │28 +pub fn indexKey(self: Self, allocator: std.mem.Allocator) ![]const u8 {│ │29 + _ = allocator; │ │30 + return self.name; │ │31 +} │ │32 │ │33 pub fn validateName(name: []const u8) !void { │ │34 if (name.len == 0) return error.NameEmpty; │ ╰───────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │77 event_maybe: ?@This(), │ │78 arena: *std.heap.ArenaAllocator, │ │79 created_ts: u64, │ │80 + // the commit this event came from, recorded against every field it │ │81 + // changes. null from `merge`, which sets oids and conflicts itself.│ │82 + event_oid: ?[]const u8, │ │83 ) !void { │ │84 const user_key = hash.hashInt(hash_kind, event_id); │ │85 │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 89 const name_to_user_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "name->user-id")); │ │ 90 const name_to_user_id = try DB.HashMap(.read_write).init(name_to_user_id_cursor); │ │ 91 │ │ 92 + const conflicts_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, conflicts_key)); │ │ 93 + const conflicts = try DB.SortedMap(.read_write).init(conflicts_cursor); │ │ 94 + │ │ 95 + const user_id_to_field_to_oid_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, id_to_field_to_oid_key));│ │ 96 + const user_id_to_field_to_oid = try DB.HashMap(.read_write).init(user_id_to_field_to_oid_cursor); │ │ 97 + │ │ 98 if (event_maybe) |event| { │ │ 99 try validateName(event.name); │ │100 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │102 │ │103 // if this event_id already maps to a user with a different name, │ │104 // drop the stale name->id entry first │ │105 + var existing_event_maybe: ?@This() = null; │ │106 const existing_cursor_maybe = try event_id_to_user.getCursor(user_key); │ │107 if (existing_cursor_maybe) |existing_cursor| { │ │108 const existing_user = try DB.HashMap(.read_only).init(existing_cursor);│ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │109 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_user);│ │110 + existing_event_maybe = existing_event; │ │111 // updates preserve the original creation timestamp │ │112 event_to_write.created_ts = existing_event.created_ts; │ │113 if (!std.mem.eql(u8, existing_event.name, event.name)) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ │114 _ = try name_to_user_id.remove(hash.hashInt(hash_kind, existing_event.name)); │ │115 } │ │116 + │ │117 + // any event settles the conflict, since resolving in the ui may │ │118 + // keep our own values and so change nothing to detect │ │119 + if (event_oid != null) { │ │120 + _ = try conflicts.remove(&evt.orderKeyDesc(existing_event.created_ts, event_id));│ │121 + } │ │122 } else { │ │123 // first time we've seen this user: stamp it with the commit timestamp │ │124 event_to_write.created_ts = created_ts; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯