╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │10 email: []const u8, │ │11 password_hash: []const u8, │ │12 enable_ansi: bool = true, │ │13 -// newline-separated authorized_keys lines (one OpenSSH public key per line) │ │14 -ssh_keys: []const u8 = "", │ │13 +ssh_keys: []const u8 = "", // newline-separated authorized_keys lines (one OpenSSH public key per line)│ │14 +created_ts: u64 = 0, // the commit timestamp of the event that first created this user │ │15 │ │16 const Self = @This(); │ │17 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │18 pub const name_max_len = 32; │ │19 + │ │20 +// `created_ts` comes from the commit timestamp, not the event │ │21 +// payload, so it must not appear in the JSON we output │ │22 +pub fn jsonStringify(self: @This(), jw: anytype) !void { │ │23 + try jw.beginObject(); │ │24 + inline for (std.meta.fields(@This())) |field| { │ │25 + if (comptime std.mem.eql(u8, field.name, "created_ts")) continue;│ │26 + try jw.objectField(field.name); │ │27 + try jw.write(@field(self, field.name)); │ │28 + } │ │29 + try jw.endObject(); │ │30 +} │ │31 │ │32 // the subset of a user that's safe to hand to clients │ │33 pub const Safe = struct { │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │59 if (event_maybe) |event| { │ │60 if (event.name.len > name_max_len) return error.NameTooLong; │ │61 │ │62 + var event_to_write = event; │ │63 + │ │64 // if this event_id already maps to a user with a different name, │ │65 // drop the stale name->id entry first │ │66 const existing_cursor_maybe = try event_id_to_user.getCursor(user_key);│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │67 if (existing_cursor_maybe) |existing_cursor| { │ │68 const existing_user = try DB.HashMap(.read_only).init(existing_cursor); │ │69 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_user);│ │70 + // updates preserve the original creation timestamp │ │71 + event_to_write.created_ts = existing_event.created_ts; │ │72 if (!std.mem.eql(u8, existing_event.name, event.name)) { │ │73 _ = try name_to_user_id.remove(hash.hashInt(hash_kind, existing_event.name)); │ │74 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │75 + } else { │ │76 + // first time we've seen this user: stamp it with the commit timestamp│ │77 + event_to_write.created_ts = created_ts; │ │78 } │ │79 │ │80 const user_cursor = try event_id_to_user.putCursor(user_key); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │81 const user = try DB.HashMap(.read_write).init(user_cursor); │ │63 - try evt.upsert(@This(), DB, hash_kind, user, event); │ │82 + try evt.upsert(@This(), DB, hash_kind, user, event_to_write); │ │83 │ │84 try name_to_user_id.put(hash.hashInt(hash_kind, event.name), .{ .bytes = event_id });│ │85 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │67 - // first time we've seen this user: record its creation timestamp and add it │ │68 - // to the ordered map the users view paginates through │ │86 + // first time we've seen this user: add it to the ordered map the users │ │87 + // view paginates through │ │88 if (existing_cursor_maybe == null) { │ │70 - try user.put(hash.hashInt(hash_kind, "created-ts"), .{ .uint = created_ts }); │ │71 - │ │89 const timestamp_to_user_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "timestamp->user-id"));│ │90 const timestamp_to_user_id = try DB.SortedMap(.read_write).init(timestamp_to_user_id_cursor); │ │74 - const order_key = evt.orderKey(created_ts, event_id); │ │91 + const order_key = evt.orderKey(event_to_write.created_ts, event_id); │ │92 try timestamp_to_user_id.put(&order_key, .{ .bytes = event_id }); │ │93 } │ │94 } else { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 99 _ = try name_to_user_id.remove(hash.hashInt(hash_kind, existing_event.name)); │ │100 │ │101 // drop it from the ordered map using its recorded creation timestamp │ │ 85 - if (try existing_user.getCursor(hash.hashInt(hash_kind, "created-ts"))) |created_ts_cursor| { │ │ 86 - const stored_ts = try created_ts_cursor.readUint(); │ │ 87 - const timestamp_to_user_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "timestamp->user-id"));│ │ 88 - const timestamp_to_user_id = try DB.SortedMap(.read_write).init(timestamp_to_user_id_cursor); │ │ 89 - const order_key = evt.orderKey(stored_ts, event_id); │ │ 90 - _ = try timestamp_to_user_id.remove(&order_key); │ │ 91 - } │ │102 + const timestamp_to_user_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "timestamp->user-id")); │ │103 + const timestamp_to_user_id = try DB.SortedMap(.read_write).init(timestamp_to_user_id_cursor); │ │104 + const order_key = evt.orderKey(existing_event.created_ts, event_id); │ │105 + _ = try timestamp_to_user_id.remove(&order_key); │ │106 } │ │107 │ │108 if (!try event_id_to_user.remove(user_key)) return error.EventNotFound; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯