╭ changed ────╮ │build.zig.zon│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 3 .version = "0.0.0", │ │ 4 .dependencies = .{ │ │ 5 .xit = .{ │ │ 6 - .url = "git+https://github.com/xit-vcs/xit#9de918726a6e50c8fa1ec5fedcac079d3587b014",│ │ 7 - .hash = "xit-0.0.0-Ub3nDNLvTwG5iWmglRVQ-ICg4jTr-MCya5Tw_iaIW7q3", │ │ 6 + .url = "git+https://github.com/xit-vcs/xit#f2cb14a3eb1fe26c91fd1b57e7ef738d243c40d7",│ │ 7 + .hash = "xit-0.0.0-Ub3nDCrwTwH3EzuMbizWrKLcBT8eAJwmUpAPKYyRufaB", │ │ 8 }, │ │ 9 }, │ │10 .paths = .{ │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │35 repo: ?Repo, │ │36 issue: ?Issue, │ │37 }, │ │38 + timestamp: u64 = 0, // not serialized, because it comes from the commit timestamp│ │39 │ │40 pub fn jsonStringify(self: EventWithId, jw: anytype) !void { │ │41 try jw.beginObject(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │311 var current_event_id: [event_id_size]u8 = undefined; │ │312 _ = try std.fmt.hexToBytes(&current_event_id, &event_with_id.id); │ │313 │ │314 + const created_ts = commit_object.content.commit.metadata.timestamp; │ │315 + │ │316 switch (event_with_id.event) { │ │314 - .user => |event_maybe| try User.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena), │ │315 - .repo => |event_maybe| try Repo.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena), │ │317 + .user => |event_maybe| try User.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts),│ │318 + .repo => |event_maybe| try Repo.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe, &arena, created_ts),│ │319 .issue => |event_maybe| try Issue.consume(DB, repo_opts.hash, haxy_moment, &current_event_id, event_maybe), │ │320 } │ │321 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │365 for (events) |event| { │ │366 json.clearRetainingCapacity(); │ │367 try std.json.Stringify.value(event, .{}, &json.writer); │ │365 - _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, ref); │ │368 + _ = try repo.commitAtRef(io, allocator, .{ .message = json.written(), .timestamp = event.timestamp }, null, ref);│ │369 } │ │370 │ │371 try consume(repo_opts, io, allocator, repo, ref); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │372 +} │ │373 + │ │374 +// build the key for SortedMaps sorted by timestamp. the big-endian timestamp │ │375 +// makes byte order match creation order; the event id breaks ties and keeps │ │376 +// keys unique within the same timestamp. │ │377 +pub fn orderKey(timestamp: u64, event_id: *const [event_id_size]u8) [@sizeOf(u64) + event_id_size]u8 {│ │378 + var key: [@sizeOf(u64) + event_id_size]u8 = undefined; │ │379 + std.mem.writeInt(u64, key[0..@sizeOf(u64)], timestamp, .big); │ │380 + @memcpy(key[@sizeOf(u64)..], event_id); │ │381 + return key; │ │382 } │ │383 │ │384 // split a pushed "<owner>/<repo>" path into its two components, or null if it │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │436 │ │437 try commitAndConsume(admin_repo_opts, io, allocator, &repo, events_ref, &[_]EventWithId{.{│ │438 .id = event_id_hex, │ │439 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │440 .event = .{ .repo = .{ │ │441 .user_id = &owner_user_id, │ │442 .name = repo_name, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭───────────────────────────────────────────────────────────╮ │19 event_id: *const [evt.event_id_size]u8, │ │20 event_maybe: ?@This(), │ │21 arena: *std.heap.ArenaAllocator, │ │22 + created_ts: u64, │ │23 ) !void { │ │24 const repo_key = hash.hashInt(hash_kind, event_id);│ │25 │ ╰───────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │54 │ │55 try name_to_repo_id.put(hash.hashInt(hash_kind, repo_path), .{ .bytes = event_id }); │ │56 │ │56 - // first time we've seen this repo: append its id to the ordered list │ │57 - // the repos view paginates through (a delete leaves a tombstone here). │ │57 + // first time we've seen this repo: record its creation timestamp and add it │ │58 + // to the ordered map the repos view paginates through │ │59 if (existing_cursor_maybe == null) { │ │59 - const repo_list_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "repo-list")); │ │60 - const repo_list = try DB.ArrayList(.read_write).init(repo_list_cursor); │ │61 - try repo_list.append(.{ .bytes = event_id }); │ │60 + try repo.put(hash.hashInt(hash_kind, "created-ts"), .{ .uint = created_ts }); │ │61 + │ │62 + const timestamp_to_repo_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "timestamp->repo-id"));│ │63 + const timestamp_to_repo_id = try DB.SortedMap(.read_write).init(timestamp_to_repo_id_cursor); │ │64 + const order_key = evt.orderKey(created_ts, event_id); │ │65 + try timestamp_to_repo_id.put(&order_key, .{ .bytes = event_id }); │ │66 } │ │67 │ │68 const user_id_to_repos_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repos")); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │79 const existing_path = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ existing_repo_event.user_id, existing_repo_event.name });│ │80 _ = try name_to_repo_id.remove(hash.hashInt(hash_kind, existing_path)); │ │81 │ │82 + // drop it from the ordered map using its recorded creation timestamp │ │83 + if (try existing_repo.getCursor(hash.hashInt(hash_kind, "created-ts"))) |created_ts_cursor| { │ │84 + const stored_ts = try created_ts_cursor.readUint(); │ │85 + const timestamp_to_repo_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "timestamp->repo-id")); │ │86 + const timestamp_to_repo_id = try DB.SortedMap(.read_write).init(timestamp_to_repo_id_cursor); │ │87 + const order_key = evt.orderKey(stored_ts, event_id); │ │88 + _ = try timestamp_to_repo_id.remove(&order_key); │ │89 + } │ │90 + │ │91 const user_id_to_repos_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repos")); │ │92 const user_id_to_repos = try DB.HashMap(.read_write).init(user_id_to_repos_cursor); │ │93 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/User.zig│ ╰──────────────────╯ ╭───────────────────────────────────────────────────────────╮ │34 event_id: *const [evt.event_id_size]u8, │ │35 event_maybe: ?@This(), │ │36 arena: *std.heap.ArenaAllocator, │ │37 + created_ts: u64, │ │38 ) !void { │ │39 const user_key = hash.hashInt(hash_kind, event_id);│ │40 │ ╰───────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯