╭ changed ────╮ │src/event.zig│ ╰─────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │482 } │ │483 }, │ │484 .bool => @field(event, field.name) = try readBool(DB, hash_kind, map, field.name), │ │485 + .int => |int_info| switch (int_info.signedness) { │ │486 + .unsigned => @field(event, field.name) = @intCast(try readUint(DB, hash_kind, map, field.name)),│ │487 + .signed => @field(event, field.name) = @intCast(try readInt(DB, hash_kind, map, field.name)), │ │488 + }, │ │489 else => @compileError("unsupported read field type: " ++ @typeName(field.type)), │ │490 } │ │491 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │505 ) ![]const u8 { │ │506 const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound;│ │507 return try cursor.readBytesAlloc(allocator, null); │ │508 +} │ │509 + │ │510 +fn readUint( │ │511 + comptime DB: type, │ │512 + comptime hash_kind: hash.HashKind, │ │513 + map: DB.HashMap(.read_only), │ │514 + field_name: []const u8, │ │515 +) !u64 { │ │516 + const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound;│ │517 + return try cursor.readUint(); │ │518 +} │ │519 + │ │520 +fn readInt( │ │521 + comptime DB: type, │ │522 + comptime hash_kind: hash.HashKind, │ │523 + map: DB.HashMap(.read_only), │ │524 + field_name: []const u8, │ │525 +) !i64 { │ │526 + const cursor = try map.getCursor(hash.hashInt(hash_kind, field_name)) orelse return error.NotFound;│ │527 + return try cursor.readInt(); │ │528 } │ │529 │ │530 fn readBool( │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────╮ │src/event/Repo.zig│ ╰──────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │ 7 name: []const u8, │ │ 8 description: []const u8, │ │ 9 enable_issue: bool, │ │10 +created_ts: u64 = 0, // the commit timestamp of the event that first created this repo│ │11 │ │12 const Self = @This(); │ │13 │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │14 pub const name_max_len = 32; │ │15 │ │16 +// `created_ts` comes from the commit timestamp, not the event │ │17 +// payload, so it must not appear in the JSON we output │ │18 +pub fn jsonStringify(self: @This(), jw: anytype) !void { │ │19 + try jw.beginObject(); │ │20 + inline for (std.meta.fields(@This())) |field| { │ │21 + if (comptime std.mem.eql(u8, field.name, "created_ts")) continue;│ │22 + try jw.objectField(field.name); │ │23 + try jw.write(@field(self, field.name)); │ │24 + } │ │25 + try jw.endObject(); │ │26 +} │ │27 + │ │28 pub fn consume( │ │29 comptime DB: type, │ │30 comptime hash_kind: hash.HashKind, │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │45 if (event_maybe) |event| { │ │46 if (event.name.len > name_max_len) return error.NameTooLong; │ │47 │ │48 + var event_to_write = event; │ │49 + │ │50 // the index is keyed by "user-id/repo-name": a repo is unique per │ │51 // owner, and the read side resolves the url's username to its user id. │ │52 const repo_path = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ event.user_id, event.name });│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │57 if (existing_cursor_maybe) |existing_cursor| { │ │58 const existing_repo = try DB.HashMap(.read_only).init(existing_cursor); │ │59 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_repo); │ │60 + // updates preserve the original creation timestamp │ │61 + event_to_write.created_ts = existing_event.created_ts; │ │62 const existing_path = try std.fmt.allocPrint(arena.allocator(), "{s}/{s}", .{ existing_event.user_id, existing_event.name });│ │63 if (!std.mem.eql(u8, existing_path, repo_path)) { │ │64 _ = try name_to_repo_id.remove(hash.hashInt(hash_kind, existing_path)); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────╮ │65 } │ │66 + } else { │ │67 + // first time we've seen this repo: stamp it with the commit timestamp│ │68 + event_to_write.created_ts = created_ts; │ │69 } │ │70 │ │71 const repo_cursor = try event_id_to_repo.putCursor(repo_key); │ ╰──────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────╮ │72 const repo = try DB.HashMap(.read_write).init(repo_cursor); │ │53 - try evt.upsert(@This(), DB, hash_kind, repo, event); │ │73 + try evt.upsert(@This(), DB, hash_kind, repo, event_to_write); │ │74 │ │75 try name_to_repo_id.put(hash.hashInt(hash_kind, repo_path), .{ .bytes = event_id });│ │76 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 │ │77 + // first time we've seen this repo: add it to the ordered map the repos │ │78 + // view paginates through │ │79 if (existing_cursor_maybe == null) { │ │60 - try repo.put(hash.hashInt(hash_kind, "created-ts"), .{ .uint = created_ts }); │ │61 - │ │80 const timestamp_to_repo_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "timestamp->repo-id"));│ │81 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); │ │82 + const order_key = evt.orderKey(event_to_write.created_ts, event_id); │ │83 try timestamp_to_repo_id.put(&order_key, .{ .bytes = event_id }); │ │84 } │ │85 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 98 _ = try name_to_repo_id.remove(hash.hashInt(hash_kind, existing_path)); │ │ 99 │ │100 // 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 - } │ │101 + const timestamp_to_repo_id_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "timestamp->repo-id")); │ │102 + const timestamp_to_repo_id = try DB.SortedMap(.read_write).init(timestamp_to_repo_id_cursor); │ │103 + const order_key = evt.orderKey(existing_repo_event.created_ts, event_id); │ │104 + _ = try timestamp_to_repo_id.remove(&order_key); │ │105 │ │106 const user_id_to_repos_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "user-id->repos")); │ │107 const user_id_to_repos = try DB.HashMap(.read_write).init(user_id_to_repos_cursor); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯