╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────╮ │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; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │125 } │ │126 │ │127 + try evt.writeOid(@This(), DB, hash_kind, user_id_to_field_to_oid, user_key, existing_event_maybe, event, event_oid);│ │128 + │ │129 const user_cursor = try event_id_to_user.putCursor(user_key); │ │130 const user = try DB.HashMap(.read_write).init(user_cursor); │ │131 try evt.upsert(@This(), DB, hash_kind, user, event_to_write); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │133 try name_to_user_id.put(hash.hashInt(hash_kind, event.name), .{ .bytes = event_id }); │ │134 │ │135 // first time we've seen this user: add it to the ordered set the users │ │104 - // view paginates through. the key (orderKey) embeds the event id, so the │ │105 - // set needs no value. │ │136 + // view paginates through. the key embeds the event id, so the set needs │ │137 + // no value. │ │138 if (existing_cursor_maybe == null) { │ │139 const user_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id-set"));│ │140 const user_id_set = try DB.SortedSet(.read_write).init(user_id_set_cursor); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │109 - const order_key = evt.orderKey(event_to_write.created_ts, event_id); │ │141 + const order_key = evt.orderKeyDesc(event_to_write.created_ts, event_id);│ │142 try user_id_set.put(&order_key); │ │143 } │ │144 } else { │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │151 // drop it from the ordered set using its recorded creation timestamp │ │152 const user_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id-set"));│ │153 const user_id_set = try DB.SortedSet(.read_write).init(user_id_set_cursor); │ │122 - const order_key = evt.orderKey(existing_event.created_ts, event_id); │ │154 + const order_key = evt.orderKeyDesc(existing_event.created_ts, event_id); │ │155 _ = try user_id_set.remove(&order_key); │ │156 + │ │157 + _ = try conflicts.remove(&order_key); │ │158 + _ = try user_id_to_field_to_oid.remove(user_key); │ │159 } │ │160 │ │161 if (!try event_id_to_user.remove(user_key)) return error.EventNotFound; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯