╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────╮ │src/test/event.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │854 moment: DB.HashMap(.read_only), │ │855 expected: []const *const [evt.event_id_size]u8, │ │856 ) !void { │ │855 - const cursor = try moment.getCursor(hash.hashInt(hash_kind, "created-ts->repo-id")) orelse return error.NotFound;│ │856 - const map = try DB.SortedMap(.read_only).init(cursor); │ │857 - try std.testing.expectEqual(expected.len, try map.count()); │ │857 + const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; │ │858 + const set = try DB.SortedSet(.read_only).init(cursor); │ │859 + try std.testing.expectEqual(expected.len, try set.count()); │ │860 for (expected, 0..) |id, i| { │ │859 - const kv = (try map.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; │ │860 - var buf: [evt.event_id_size]u8 = undefined; │ │861 - _ = try kv.value_cursor.readBytes(&buf); │ │862 - try std.testing.expectEqualSlices(u8, id, &buf); │ │861 + const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; │ │862 + // the key is orderKey ([timestamp][event-id]); its trailing bytes are the id │ │863 + var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │864 + _ = try kv.key_cursor.readBytes(&order_key); │ │865 + try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); │ │866 } │ │867 } │ │868 }; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │882 const moment = try evt.currentMoment(repo_opts, &repo); │ │883 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[1], &repo_ids[2], &repo_ids[3] }); │ │884 │ │882 - // the single user shows up in its own ordered map │ │883 - const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "created-ts->user-id")) orelse return error.NotFound;│ │884 - const umap = try Repo.DB.SortedMap(.read_only).init(ucur); │ │885 - try std.testing.expectEqual(1, try umap.count()); │ │885 + // the single user shows up in its own ordered set │ │886 + const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; │ │887 + const uset = try Repo.DB.SortedSet(.read_only).init(ucur); │ │888 + try std.testing.expectEqual(1, try uset.count()); │ │889 } │ │890 │ │891 // delete repo1 -> dense order (no tombstone) │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │41 else => 0, │ │42 }) }, │ │43 .user => switch (route) { │ │44 - .user, .user_settings, .user_auth => |name| .{ .user = try User.init(arena, haxy_moment, name) },│ │44 + .user => |u| .{ .user = try User.init(arena, haxy_moment, u.name, u.after) }, │ │45 + .user_settings, .user_auth => |name| .{ .user = try User.init(arena, haxy_moment, name, 0) }, │ │46 else => return error.UnexpectedRoute, │ │47 }, │ │48 .repo => switch (route) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────╮ │69 home_repos: usize, // 0 = first page │ │70 home_settings, │ │71 home_auth, │ │71 - user: Array(evt.User.name_max_len), │ │72 + user: struct { name: Array(evt.User.name_max_len), after: usize = 0 },│ │73 user_settings: Array(evt.User.name_max_len), │ │74 user_auth: Array(evt.User.name_max_len), │ │75 repo: Array(repo_route_max_len), │ ╰──────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │233 return switch (self) { │ │234 .home_users => |after| if (after == 0) @as([]const u8, "/users") else try std.fmt.allocPrint(arena.allocator(), "/users?after={d}", .{after}),│ │235 .home_repos => |after| if (after == 0) @as([]const u8, "/repos") else try std.fmt.allocPrint(arena.allocator(), "/repos?after={d}", .{after}),│ │235 - .user => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}", .{name.slice()}), │ │236 + .user => |u| if (u.after == 0) │ │237 + try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}", .{u.name.slice()}) │ │238 + else │ │239 + try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}?after={d}", .{ u.name.slice(), u.after }), │ │240 .user_settings => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/settings", .{name.slice()}), │ │241 .user_auth => |name| try std.fmt.allocPrint(arena.allocator(), user_segment ++ "{s}/auth", .{name.slice()}), │ │242 .repo => |name| try std.fmt.allocPrint(arena.allocator(), repo_segment ++ "{s}", .{name.slice()}), │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────╮ │283 if (std.mem.eql(u8, sub, "auth")) return .{ .user_auth = parsed };│ │284 return null; // unknown sub-path │ │285 } │ │282 - return .{ .user = parsed }; │ │286 + return .{ .user = .{ .name = parsed, .after = after } }; │ │287 } │ │288 // /repo/<username>/<reponame> (files root), then optionally /settings, │ │289 // /auth, or /files/<dir> for a directory in the files tab. the `repo` │ ╰───────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │366 return switch (a) { │ │367 .home_users => |a_after| a_after == b.home_users, │ │368 .home_repos => |a_after| a_after == b.home_repos, │ │365 - .user => |a_name| std.mem.eql(u8, a_name.slice(), b.user.slice()), │ │369 + .user => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user.name.slice()) and a_u.after == b.user.after,│ │370 .user_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.user_settings.slice()), │ │371 .user_auth => |a_name| std.mem.eql(u8, a_name.slice(), b.user_auth.slice()), │ │372 .repo => |a_name| std.mem.eql(u8, a_name.slice(), b.repo.slice()), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │388 return !a.eql(b); │ │389 } │ │390 │ │391 + // true when `a` and `b` are the same user paginated to a different repos │ │392 + // window. switching between a user's tabs is in-page (header-handled), so │ │393 + // only a changed `after` on the repos list navigates. │ │394 + pub fn userPageChanged(a: RoutablePage, b: RoutablePage) bool { │ │395 + return switch (a) { │ │396 + .user => |aa| switch (b) { │ │397 + .user => |bb| std.mem.eql(u8, aa.name.slice(), bb.name.slice()) and aa.after != bb.after,│ │398 + else => false, │ │399 + }, │ │400 + else => false, │ │401 + }; │ │402 + } │ │403 + │ │404 // true when `a` and `b` are the same home list tab paginated to a different │ │405 // window. switching between the users/repos tabs is in-page (the home page │ │406 // holds both lists), so only a changed `after` on the same tab navigates. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────╮ │427 try jw.objectField("kind"); │ │428 try jw.write(@tagName(self)); │ │429 switch (self) { │ │413 - .user, .user_settings, .user_auth => |name| {│ │430 + .user => |u| { │ │431 + try jw.objectField("name"); │ │432 + try jw.write(u.name.slice()); │ │433 + try jw.objectField("after"); │ │434 + try jw.write(u.after); │ │435 + }, │ │436 + .user_settings, .user_auth => |name| { │ │437 try jw.objectField("name"); │ │438 try jw.write(name.slice()); │ │439 }, │ ╰──────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │481 .home_repos => .{ .home_repos = helper.after }, │ │482 .home_settings => .home_settings, │ │483 .home_auth => .home_auth, │ │461 - .user => .{ .user = try parseName(evt.User.name_max_len, helper.name) }, │ │484 + .user => .{ .user = .{ .name = try parseName(evt.User.name_max_len, helper.name), .after = helper.after } },│ │485 .user_settings => .{ .user_settings = try parseName(evt.User.name_max_len, helper.name) }, │ │486 .user_auth => .{ .user_auth = try parseName(evt.User.name_max_len, helper.name) }, │ │487 .repo => .{ .repo = try parseName(repo_route_max_len, helper.name) }, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯