╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │421 view: EventsView = .active, │ │422 kind: ?evt.EventKind = null, │ │423 selected: Array(evt.event_id_size * 2) = .{}, │ │424 - // limits the list to the events one transaction wrote │ │424 + // a transaction's index in the repo history, limiting the list to the│ │425 + // events it wrote. the page reads that transaction's own state │ │426 moment: ?u64 = null, │ │427 }; │ │428 │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Events.zig│ ╰──────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │74 const gpa = arena.child_allocator; │ │75 var event_db_maybe: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, gpa, repo.core.repo_dir) else null;│ │76 defer if (event_db_maybe) |*event_db| event_db.deinit(io, gpa); │ │77 - const haxy_moment = (if (event_db_maybe) |*event_db| │ │77 + const haxy_moment = (if (moment_index) |index| │ │78 + transactionMoment(repo_kind, repo_opts, repo, index) │ │79 + else if (event_db_maybe) |*event_db| │ │80 evt.currentMomentFromDb(repo_opts.hash, event_db.db) │ │81 else if (repo_kind == .git) │ │82 return result │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │109 root_key = evt.orderKeyDesc(meta.updated_order, &id); │ │110 } │ │111 │ │110 - if (moment_index) |index| { │ │111 - try readMomentEvents(repo_kind, repo_opts, arena, &result, haxy_moment, kind_map, admin_moment, identity, index, selected);│ │112 + if (moment_index != null) { │ │113 + try readMomentEvents(repo_kind, repo_opts, arena, &result, haxy_moment, kind_map, admin_moment, identity, selected); │ │114 return result; │ │115 } │ │116 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │148 return result; │ │149 } │ │150 │ │151 +// the haxy state as of one transaction in the repo history. a moment page │ │152 +// reads from it, so undoing or rebasing later transactions cannot change what │ │153 +// it shows, and the events read as they were when the transaction ran │ │154 +fn transactionMoment( │ │155 + comptime repo_kind: rp.RepoKind, │ │156 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │157 + repo: *rp.Repo(repo_kind, repo_opts), │ │158 + index: u64, │ │159 +) !evt.EventDB(repo_opts.hash).HashMap(.read_only) { │ │160 + if (repo_kind == .git) return error.NotFound; │ │161 + const DB = evt.EventDB(repo_opts.hash); │ │162 + const history = try DB.ArrayList(.read_only).init(repo.core.db.rootCursor().readOnly()); │ │163 + const cursor = try history.getCursor(index) orelse return error.NotFound; │ │164 + return try evt.currentMomentFromRepoMoment(repo_opts.hash, try DB.HashMap(.read_only).init(cursor));│ │165 +} │ │166 + │ │167 // the moment's set is keyed on the event order, not the update order the │ │168 // active and removed sets use, so it gets its own walk. one transaction's │ │169 // events are few, so the whole set is scanned to count both views. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────╮ │176 kind_map: evt.EventDB(repo_opts.hash).HashMap(.read_only),│ │177 admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │178 identity: []const u8, │ │161 - moment_index: u64, │ │179 selected: []const u8, │ │180 ) !void { │ │181 const DB = evt.EventDB(repo_opts.hash); │ ╰───────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │182 const aa = arena.allocator(); │ │166 - const ids = try evt.momentEventIds(DB, repo_opts.hash, haxy_moment, moment_index) orelse return error.NotFound; │ │183 + // the moment names its own position, so the page needs no index from the │ │184 + // url. a transaction that consumed nothing has no set of its own, which │ │185 + // leaves the list empty rather than failing the page: the terminal hosts │ │186 + // build their pages inside the run loop, where an error ends the session │ │187 + const index_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.moment_index_key)) orelse return;│ │188 + const ids = try evt.momentEventIds(DB, repo_opts.hash, haxy_moment, try index_cursor.readUint()) orelse return; │ │189 │ │190 var events: [2]std.ArrayList(Item) = .{ .empty, .empty }; │ │191 // the selected event's window starts at it, not at the top │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │214 const item = (try readItem(repo_opts.hash, arena, haxy_moment, kind_map, admin_moment, identity, view == .active, kind, &id)) orelse continue;│ │215 try window_events.append(aa, item); │ │216 } │ │195 - if (awaiting_selected) return error.NotFound; │ │217 + // the window start can name an event this batch no longer holds │ │218 + if (awaiting_selected) return; │ │219 result.active.events = events[@intFromEnum(ui.RoutablePage.EventsView.active)].items; │ │220 result.removed.events = events[@intFromEnum(ui.RoutablePage.EventsView.removed)].items; │ │221 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────╮ │src/ui/Repo/Undo.zig│ ╰────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │55 const moment = try DB.HashMap(.read_only).init(cursor); │ │56 const record = try xit.undo.read(opts, moment, &buffer); │ │57 var description: std.Io.Writer.Allocating = .init(aa); │ │58 - const detail = if (record) |value| try eventDetail(opts, arena, identity, value, moment) else null; │ │58 + const detail = if (record) |value| try eventDetail(opts, arena, identity, value, moment, remaining) else null;│ │59 if (detail) |value| { │ │60 try description.writer.writeAll(value.description); │ │61 } else if (record) |value| { │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │92 identity: []const u8, │ │93 record: xit.undo.UndoRecord, │ │94 moment: rp.Repo(.xit, opts).DB.HashMap(.read_only), │ │95 + history_index: u64, │ │96 ) !?Detail { │ │97 if (std.mem.eql(u8, record.action, evt.merge_undo_action)) return .{ .action = "merge events", .description = "merged the events ref from a remote" };│ │98 if (!std.mem.eql(u8, record.action, evt.undo_action)) return null; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 99 │ │100 const description = "updated the event database"; │ │100 - const batch = try momentBatch(opts, moment) orelse return .{ .action = "events", .description = description }; │ │101 + const count = try momentEventCount(opts, moment) orelse return .{ .action = "events", .description = description }; │ │102 │ │103 + // the page reads the transaction's own moment, so the url names the │ │104 + // transaction rather than the state it produced │ │105 const aa = arena.allocator(); │ │103 - const route = ui.RoutablePage.repoEventsRoute(identity, .active, null, "", batch.index) orelse return error.RouteTooLong; │ │106 + const route = ui.RoutablePage.repoEventsRoute(identity, .active, null, "", history_index) orelse return error.RouteTooLong;│ │107 return .{ │ │105 - .action = try std.fmt.allocPrint(aa, "events ({d})", .{batch.count}), │ │108 + .action = try std.fmt.allocPrint(aa, "events ({d})", .{count}), │ │109 .description = description, │ │110 .events = .{ │ │108 - .label = try std.fmt.allocPrint(aa, "view events ({d})", .{batch.count}), │ │111 + .label = try std.fmt.allocPrint(aa, "view events ({d})", .{count}), │ │112 .link = try std.fmt.allocPrint(aa, "a:{s}", .{try route.toUrl(arena)}), │ │113 }, │ │114 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯