╭ changed ──────╮ │src/ui/User.zig│ ╰───────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭────────────────────────────────────────────────────────────╮ │22 │ │23 pub const ForkItem = struct { │ │24 id: []const u8, │ │25 + // the fork route's identity, which names the forker│ │26 + identity: []const u8, │ │27 target: []const u8, │ │28 title: []const u8, │ │29 }; │ ╰────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────╮ │50 repos_start: usize, │ │51 forks_start: usize, │ │52 ) !Self { │ │53 + // admin and user repos share one db type │ │54 const DB = evt.AdminDB; │ │55 const hash_kind = evt.admin_repo_opts.hash;│ │56 │ ╰───────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │54 - // a route identifies a user by name; resolve it to the user's event id via │ │55 - // the name->user-id index, which everything below keys off of. │ │56 - const name_to_user_id_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "name->user-id")) orelse return error.NotFound;│ │57 - const name_to_user_id = try DB.HashMap(.read_only).init(name_to_user_id_cursor); │ │58 - const user_id_cursor = try name_to_user_id.getCursor(hash.hashInt(hash_kind, name.slice())) orelse return error.NotFound; │ │59 - var user_id_buf: [evt.event_id_size]u8 = undefined; │ │60 - _ = try user_id_cursor.readBytes(&user_id_buf); │ │61 - const user_id: []const u8 = &user_id_buf; │ │57 + // a route identifies a user by name, which everything below keys off of │ │58 + const user_id = (try evt.User.readIdByName(DB, hash_kind, haxy_moment, name.slice())) orelse return error.NotFound; │ │59 + const user = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, &user_id)) orelse return error.NotFound; │ │60 │ │63 - const user = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, user_id)) orelse return error.NotFound; │ │61 + const io = session.io orelse return error.NoMoment; │ │62 + const repos_dir = session.repos_dir orelse return error.NoMoment; │ │63 + const gpa = arena.child_allocator; │ │64 + │ │65 + // a user with no user repo or no events has empty lists │ │66 + var user_repo_maybe = try evt.openUserRepo(io, gpa, repos_dir, &user_id); │ │67 + defer if (user_repo_maybe) |*user_repo| user_repo.deinit(io, gpa); │ │68 + const user_moment = if (user_repo_maybe) |*user_repo| try evt.userMoment(user_repo) else null; │ │69 │ │70 var repos: std.ArrayList(evt.Repo.Record) = .empty; │ │71 var repos_next_start: ?usize = null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │67 - │ │68 - // the user-id->repo-id-set index maps each user to a set of their repo event │ │69 - // ids ordered by creation (newest first); it only exists once a repo has │ │70 - // been consumed, so a user with no repos simply yields an empty list. │ │71 - if (try haxy_moment.getCursor(hash.hashInt(hash_kind, "user-id->repo-id-set"))) |user_id_to_repo_id_set_cursor| { │ │72 - const user_id_to_repo_id_set = try DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); │ │73 - if (try user_id_to_repo_id_set.getCursor(hash.hashInt(hash_kind, user_id))) |user_repos_cursor| { │ │72 + if (user_moment) |moment| { │ │73 + if (try moment.getCursor(hash.hashInt(hash_kind, evt.Repo.active_id_set_key))) |user_repos_cursor| { │ │74 const user_repos = try DB.SortedSet(.read_only).init(user_repos_cursor); │ │75 const count = try user_repos.count(); │ │76 - │ │77 - const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->repo")) orelse return error.NotFound;│ │78 - const event_id_to_repo = try DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ │76 │ │77 // read the window [start, start+page_size) with one seek to the start │ │78 // rank, then a sequential walk. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │80 var repos_iter = try user_repos.iteratorFromIndex(repos_start); │ │81 var i = repos_start; │ │82 while (i < end) : (i += 1) { │ │86 - var kv_cursor = (try repos_iter.next()) orelse break; │ │87 - const kv = try kv_cursor.readKeyValuePair(); │ │88 - // the set key is orderKey = [created-order][event-id]; its trailing │ │89 - // bytes are the repo event id. │ │90 - var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │91 - _ = try kv.key_cursor.readBytes(&order_key); │ │92 - const event_id = order_key[@sizeOf(u64)..]; │ │93 - const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(hash_kind, event_id)) orelse continue;│ │94 - const repo_map = try DB.HashMap(.read_only).init(repo_cursor); │ │95 - const repo_event = try evt.read(evt.Repo.Record, DB, hash_kind, arena, repo_map); │ │83 + const kv_cursor = (try repos_iter.next()) orelse break; │ │84 + const event_id = try evt.readOrderKeyId(DB, kv_cursor); │ │85 + const repo_event = (try evt.Repo.readById(DB, hash_kind, moment, arena, &event_id)) orelse continue; │ │86 // unreadable repos leave their window short rather than shifting the others │ │87 if (evt.Repo.roleOf(repo_event, session.userId()) == .none) continue; │ │88 try repos.append(arena.allocator(), repo_event); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 93 │ │ 94 var forks: std.ArrayList(ForkItem) = .empty; │ │ 95 var forks_next_start: ?usize = null; │ │106 - if (try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Fork.user_id_to_fork_id_set_key))) |by_user_cursor| { │ │107 - const by_user = try DB.HashMap(.read_only).init(by_user_cursor); │ │108 - if (try by_user.getCursor(hash.hashInt(hash_kind, user_id))) |user_forks_cursor| { │ │ 96 + if (user_moment) |moment| { │ │ 97 + if (try moment.getCursor(hash.hashInt(hash_kind, evt.Fork.active_id_set_key))) |user_forks_cursor| { │ │ 98 const user_forks = try DB.SortedSet(.read_only).init(user_forks_cursor); │ │110 - const repos_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Repo.record_map_key)) orelse return error.NotFound;│ │111 - const repo_records = try DB.HashMap(.read_only).init(repos_cursor); │ │ 99 const count = try user_forks.count(); │ │100 const end = @min(forks_start +| page_size, count); │ │101 var iter = try user_forks.iteratorFromIndex(forks_start); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │102 var i = forks_start; │ │103 while (i < end) : (i += 1) { │ │117 - const cursor = (try iter.next()) orelse break; │ │118 - var kv_cursor = cursor; │ │119 - const kv = try kv_cursor.readKeyValuePair(); │ │120 - var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │121 - _ = try kv.key_cursor.readBytes(&order_key); │ │122 - const fork_id = order_key[@sizeOf(u64)..]; │ │123 - const fork_id_hex = std.fmt.bytesToHex(fork_id.*, .lower); │ │124 - const record = (try evt.Fork.readById(DB, hash_kind, haxy_moment, arena, fork_id)) orelse continue; │ │104 + const kv_cursor = (try iter.next()) orelse break; │ │105 + const fork_id = try evt.readOrderKeyId(DB, kv_cursor); │ │106 + const fork_id_hex = std.fmt.bytesToHex(fork_id, .lower); │ │107 + const record = (try evt.Fork.readById(DB, hash_kind, moment, arena, &fork_id)) orelse continue; │ │108 │ │126 - const repo_cursor = try repo_records.getCursor(hash.hashInt(hash_kind, record.event.repo_id)) orelse continue; │ │127 - const target_repo = try evt.read(evt.Repo.Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(repo_cursor)); │ │109 + // the target lives in its owner's user repo │ │110 + const owner_id = record.event.repo_user_id[0..evt.event_id_size]; │ │111 + const target_repo = (try evt.readRepoById(io, gpa, arena, repos_dir, owner_id, record.event.repo_id)) orelse continue; │ │112 if (evt.Repo.roleOf(target_repo, session.userId()) == .none) continue; │ │129 - const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, target_repo.event.user_id)) orelse continue; │ │130 - const target = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ owner.event.name, target_repo.event.name }); │ │113 + const owner = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, owner_id)) orelse continue; │ │114 │ │115 var title: []const u8 = "(unavailable)"; │ │133 - if (session.io) |io| if (session.repos_dir) |repos_dir| { │ │134 - const path = try fork.forkPath(arena.allocator(), repos_dir, &fork_id_hex); │ │135 - if (rp.Repo(.xit, .{}).open(io, arena.child_allocator, .{ .path = path, .require_repo_root = true })) |opened| { │ │136 - var fork_repo = opened; │ │137 - defer fork_repo.deinit(io, arena.child_allocator); │ │138 - if (evt.currentMoment(.{}, &fork_repo)) |moment| { │ │139 - if (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, moment, arena, fork_id)) |patch| title = patch.event.title; │ │140 - } else |_| {} │ │116 + const path = try fork.forkPath(arena.allocator(), repos_dir, &fork_id_hex); │ │117 + if (rp.Repo(.xit, .{}).open(io, gpa, .{ .path = path, .require_repo_root = true })) |opened| { │ │118 + var fork_repo = opened; │ │119 + defer fork_repo.deinit(io, gpa); │ │120 + if (evt.currentMoment(.{}, &fork_repo)) |fork_moment| { │ │121 + if (try evt.Patch.readById(evt.EventDB(.sha1), .sha1, fork_moment, arena, &fork_id)) |patch| title = patch.event.title;│ │122 } else |_| {} │ │142 - }; │ │123 + } else |_| {} │ │124 try forks.append(arena.allocator(), .{ │ │125 .id = try arena.allocator().dupe(u8, &fork_id_hex), │ │145 - .target = target, │ │126 + .identity = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ user.event.name, target_repo.event.name }), │ │127 + .target = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ owner.event.name, target_repo.event.name }), │ │128 .title = title, │ │129 }); │ │130 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │210 if (data.forks_start > 0) │ │211 try items.append(aa, .{ .text = "← previous", .link = try std.fmt.allocPrint(aa, "a:/user/{s}/forks/start:{d}", .{ data.user.name, data.forks_start -| page_size }) });│ │212 for (data.forks) |fork_item| { │ │231 - const route = ui.RoutablePage.forkPatchRoute(fork_item.target, fork_item.id) orelse return error.RouteTooLong; │ │213 + const route = ui.RoutablePage.forkPatchRoute(fork_item.identity, fork_item.id) orelse return error.RouteTooLong; │ │214 try items.append(aa, .{ │ │215 .text = try std.fmt.allocPrint(aa, "{s}\n{s}", .{ fork_item.target, fork_item.title }), │ │216 .link = try std.fmt.allocPrint(aa, "a:{s}", .{try route.toUrl(session.page_arena)}), │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯