╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
867 { 868 const event = evt.EventWithId{ .id = events_to_consume[1].id, .author = author, .event = .{ .issue = .{ 869 .title = "Search results ignore the archived project filter", 870 .description = second_issue.description, 871 .tags = second_issue.tags, 872 } } }; 873 try std.json.Stringify.value(event, .{}, &json.writer); 874 const oid = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, evt.events_ref); 875 _ = try std.fmt.hexToBytes(&retitle_oid, &oid); 876 } 877 878 // the other branch rewrites the second issue's description and drops one 879 // of its tags, then retags the third issue 880 { 881 json.clearRetainingCapacity(); 882 const event = evt.EventWithId{ .id = events_to_consume[1].id, .author = author, .event = .{ .issue = .{ 883 .title = second_issue.title, 884 .description = "Archived projects are ranked before the flag is applied.", 885 .tags = "bug search", 886 } } }; 887 try std.json.Stringify.value(event, .{}, &json.writer); 888 const oid = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{base_oid}, .message = json.written() }, null, other_events_ref); 889 _ = try std.fmt.hexToBytes(&rewrite_oid, &oid); 890 } 891 { 892 json.clearRetainingCapacity(); 893 const event = evt.EventWithId{ .id = events_to_consume[2].id, .author = author, .event = .{ .issue = .{ 894 .title = third_issue.title, 895 .description = third_issue.description, 896 .tags = "enhancement roadmap", 897 } } }; 898 try std.json.Stringify.value(event, .{}, &json.writer); 899 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); 900 } 901 } 902 903 { 904 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 905 defer result.deinit(); 906 907 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 908 defer merge.deinit(); 909 910 try std.testing.expect(.success == merge.result); 911 } 912 913 { 914 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 915 916 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 917 918 var second_id: [evt.event_id_size]u8 = undefined; 919 _ = try std.fmt.hexToBytes(&second_id, &events_to_consume[1].id); 920 921 // each side's edit to a different field survives, without conflicting 922 const merged = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &second_id); 923 try std.testing.expectEqualStrings("Search results ignore the archived project filter", merged.event.title); 924 try std.testing.expectEqualStrings("Archived projects are ranked before the flag is applied.", merged.event.description); 925 try std.testing.expectEqualStrings("bug search", merged.event.tags); 926 927 // each merged field is attributed to the branch it came from 928 try std.testing.expectEqualSlices(u8, &retitle_oid, &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &second_id, "title")); 929 try std.testing.expectEqualSlices(u8, &rewrite_oid, &try readOid(Repo.DB, repo_opts.hash, haxy_moment, &second_id, "description")); 930 931 const conflicts_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Issue.conflicts_key)) orelse return error.NotFound; 932 const conflicts = try Repo.DB.SortedMap(.read_only).init(conflicts_cursor); 933 try std.testing.expectEqual(0, try conflicts.count()); 934 935 // tags the merged-in side dropped don't survive in the tag index, 936 // whether we edited that issue or left it alone 937 const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag+status->issue-id-set")) orelse return error.NotFound; 938 const tag_to_issues = try Repo.DB.SortedMap(.read_only).init(tag_to_issues_cursor); 939 940 var third_id: [evt.event_id_size]u8 = undefined; 941 _ = try std.fmt.hexToBytes(&third_id, &events_to_consume[2].id); 942 const retagged = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &third_id); 943 try std.testing.expectEqualStrings("enhancement roadmap", retagged.event.tags); 944 945 try std.testing.expect(null == try tag_to_issues.getCursor("backend open")); 946 try std.testing.expect(null == try tag_to_issues.getCursor("preferences open")); 947 try std.testing.expect(null != try tag_to_issues.getCursor("roadmap open")); 948 try std.testing.expect(null != try tag_to_issues.getCursor("enhancement open")); 949 } 950 951 // 952 // a removal on the other branch carries over, unless we edited the issue, 953 // in which case the edit wins 954 // 955 956 { 957 const edited_issue = events_to_consume2[1].event.issue orelse return error.NotFound; 958 959 const base_oid = try repo.readRef(io, evt.events_ref) orelse return error.NotFound; 960 961 var json: std.Io.Writer.Allocating = .init(allocator); 962 defer json.deinit(); 963 964 // we retitle one of the two the other branch is about to drop 965 { 966 const event = evt.EventWithId{ .id = events_to_consume2[1].id, .author = author, .event = .{ .issue = .{ 967 .title = "Kept by the edit", 968 .description = edited_issue.description, 969 .tags = edited_issue.tags, 970 } } }; 971 try std.json.Stringify.value(event, .{}, &json.writer); 972 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, evt.events_ref); 973 } 974 975 // the other branch removes both 976 { 977 json.clearRetainingCapacity(); 978 const event = evt.EventWithId{ .id = events_to_consume2[0].id, .author = author, .event = .{ .issue = null } }; 979 try std.json.Stringify.value(event, .{}, &json.writer); 980 _ = try repo.commitAtRef(io, allocator, .{ .parent_oids = &.{base_oid}, .message = json.written() }, null, other_events_ref); 981 } 982 { 983 json.clearRetainingCapacity(); 984 const event = evt.EventWithId{ .id = events_to_consume2[1].id, .author = author, .event = .{ .issue = null } }; 985 try std.json.Stringify.value(event, .{}, &json.writer); 986 _ = try repo.commitAtRef(io, allocator, .{ .message = json.written() }, null, other_events_ref); 987 } 988 989 { 990 var result = try repo.switchDir(io, allocator, .{ .target = .{ .ref = evt.events_ref } }); 991 defer result.deinit(); 992 993 var merge = try repo.merge(io, allocator, .{ .kind = .full, .action = .{ .new = .{ .source = &.{.{ .ref = other_events_ref }} } } }, null); 994 defer merge.deinit(); 995 996 try std.testing.expect(.success == merge.result); 997 } 998 999 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &.{}); 1000 1001 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1002 1003 // the one we left alone is removed, and its tags left the index 1004 var dropped_id: [evt.event_id_size]u8 = undefined; 1005 _ = try std.fmt.hexToBytes(&dropped_id, &events_to_consume2[0].id); 1006 const dropped = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &dropped_id); 1007 try std.testing.expect(dropped.removed); 1008 1009 const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag+status->issue-id-set")) orelse return error.NotFound; 1010 const tag_to_issues = try Repo.DB.SortedMap(.read_only).init(tag_to_issues_cursor); 1011 try std.testing.expect(null == try tag_to_issues.getCursor("kanban open")); 1012 1013 // the one we edited survives, with our edit 1014 var kept_id: [evt.event_id_size]u8 = undefined; 1015 _ = try std.fmt.hexToBytes(&kept_id, &events_to_consume2[1].id); 1016 const kept = try readIssue(Repo.DB, repo_opts.hash, haxy_moment, &arena, &kept_id); 1017 try std.testing.expect(!kept.removed); 1018 try std.testing.expectEqualStrings("Kept by the edit", kept.event.title); 1019 } 1020 } 1021 1022 // the commit that last set `field_name` on the issue `id` names 1023 fn readOid( 1024 comptime DB: type, 1025 comptime hash_kind: hash.HashKind, 1026 haxy_moment: DB.HashMap(.read_only), 1027 id: *const [evt.event_id_size]u8, 1028 comptime field_name: []const u8, 1029 ) ![hash.byteLen(hash_kind)]u8 { 1030 const id_to_field_to_oid_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.Issue.id_to_field_to_oid_key)) orelse return error.NotFound; 1031 const id_to_field_to_oid = try DB.HashMap(.read_only).init(id_to_field_to_oid_cursor); 1032 const field_oids_cursor = try id_to_field_to_oid.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; 1033 const field_oids = try DB.SortedMap(.read_only).init(field_oids_cursor); 1034 const cursor = try field_oids.getCursor(field_name) orelse return error.NotFound; 1035 var oid: [hash.byteLen(hash_kind)]u8 = undefined; 1036 _ = try cursor.readBytes(&oid); 1037 return oid; 1038 } 1039 1040 // the issue `id` names, read out of `event-id->issue` 1041 fn readIssue( 1042 comptime DB: type, 1043 comptime hash_kind: hash.HashKind, 1044 haxy_moment: DB.HashMap(.read_only), 1045 arena: *std.heap.ArenaAllocator, 1046 id: *const [evt.event_id_size]u8, 1047 ) !evt.Issue.Record { 1048 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->issue")) orelse return error.NotFound; 1049 const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); 1050 const issue_cursor = try event_id_to_issue.getCursor(hash.hashInt(hash_kind, id)) orelse return error.NotFound; 1051 const issue_map = try DB.HashMap(.read_only).init(issue_cursor); 1052 return try evt.read(evt.Issue.Record, DB, hash_kind, arena, issue_map); 1053 } 1054 1055 test "user and repo" { 1056 const io = std.testing.io; 1057 const allocator = std.testing.allocator; 1058 const temp_dir_name = "temp-event-user-and-repo"; 1059 1060 // create the temp dir 1061 const cwd = std.Io.Dir.cwd(); 1062 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1063 if (temp_dir_or_err) |*temp_dir| { 1064 temp_dir.close(io); 1065 try cwd.deleteTree(io, temp_dir_name); 1066 } else |_| {} 1067 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1068 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1069 defer temp_dir.close(io); 1070 1071 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1072 defer allocator.free(cwd_path); 1073 1074 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 1075 defer allocator.free(work_path); 1076 1077 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 1078 const Repo = rp.Repo(.xit, repo_opts); 1079 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 1080 defer repo.deinit(io, allocator); 1081 1082 var arena = std.heap.ArenaAllocator.init(allocator); 1083 defer arena.deinit(); 1084 1085 // 1086 // define test events 1087 // 1088 1089 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1090 const user_event_id = evt.EventWithId.randomId(prng.random()); 1091 const repo_event_id = evt.EventWithId.randomId(prng.random()); 1092 1093 var first_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1094 const first_password_hash = try evt.User.hashPassword("correct horse battery staple", &first_password_hash_buf, io); 1095 1096 var second_password_hash_buf: [evt.User.password_hash_max_len]u8 = undefined; 1097 const second_password_hash = try evt.User.hashPassword("Tr0ub4dor&3", &second_password_hash_buf, io); 1098 1099 const events_to_consume = [_]evt.EventWithId{ 1100 .{ 1101 .id = std.fmt.bytesToHex(user_event_id, .lower), 1102 .author = author, 1103 .event = .{ 1104 .user = .{ 1105 .name = "alice", 1106 .email = "alice@example.test", 1107 .password_hash = first_password_hash, 1108 }, 1109 }, 1110 }, 1111 // this event edits the previous one because it has the same id 1112 .{ 1113 .id = std.fmt.bytesToHex(user_event_id, .lower), 1114 .author = author, 1115 .event = .{ 1116 .user = .{ 1117 .name = "alice", 1118 .email = "alice@example.test", 1119 .password_hash = second_password_hash, 1120 }, 1121 }, 1122 }, 1123 .{ 1124 .id = std.fmt.bytesToHex(repo_event_id, .lower), 1125 .author = author, 1126 .event = .{ 1127 .repo = .{ 1128 .user_id = &user_event_id, 1129 .name = "ziglings", 1130 .description = "Learn the Zig programming language by fixing tiny broken programs", 1131 }, 1132 }, 1133 }, 1134 }; 1135 1136 // commit and consume the seed events 1137 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 1138 1139 { 1140 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1141 1142 // get the map of users 1143 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1144 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1145 1146 // get the user out of the map that was edited 1147 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1148 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 1149 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 1150 1151 // the password was correctly edited 1152 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.event.password_hash); 1153 1154 // get the map of repos 1155 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1156 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1157 1158 // get the repo out of the map 1159 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1160 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1161 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1162 1163 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.event.user_id); 1164 try std.testing.expectEqualStrings("ziglings", repo_event.event.name); 1165 1166 // get the repos created by the user 1167 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 1168 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 1169 1170 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1171 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 1172 1173 try std.testing.expectEqual(1, try user_repos.count()); 1174 1175 // the set is keyed by orderKeyDesc([created-ts][event-id]) 1176 const order_key = evt.orderKeyDesc(repo_event.created_ts, &repo_event_id); 1177 try std.testing.expect(try user_repos.contains(&order_key)); 1178 } 1179 1180 // 1181 // remove the repo 1182 // 1183 1184 const events_to_consume2 = [_]evt.EventWithId{ 1185 .{ 1186 .id = std.fmt.bytesToHex(repo_event_id, .lower), 1187 .author = author, 1188 .event = .{ .repo = null }, 1189 }, 1190 }; 1191 1192 // commit and consume the removal 1193 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume2); 1194 1195 { 1196 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1197 1198 // get the map of repos 1199 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1200 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1201 1202 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1203 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1204 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1205 try std.testing.expect(repo_event.removed); 1206 1207 // get the repos created by the user 1208 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 1209 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 1210 1211 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1212 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 1213 1214 // removing the repo emptied the user's set 1215 try std.testing.expectEqual(0, try user_repos.count()); 1216 } 1217 1218 // a non-null payload restores the repo and its indexes 1219 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 1220 .id = std.fmt.bytesToHex(repo_event_id, .lower), 1221 .author = author, 1222 .event = events_to_consume[2].event, 1223 }}); 1224 1225 { 1226 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1227 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1228 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1229 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1230 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1231 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1232 try std.testing.expect(!repo_event.removed); 1233 1234 const user_id_to_repo_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "user-id->repo-id-set")) orelse return error.NotFound; 1235 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 1236 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1237 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 1238 try std.testing.expectEqual(1, try user_repos.count()); 1239 } 1240 1241 // 1242 // remove the user 1243 // 1244 1245 const events_to_consume3 = [_]evt.EventWithId{ 1246 .{ 1247 .id = std.fmt.bytesToHex(user_event_id, .lower), 1248 .author = author, 1249 .event = .{ .user = null }, 1250 }, 1251 }; 1252 1253 // commit and consume the removal 1254 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 1255 1256 { 1257 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1258 1259 // get the map of users 1260 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1261 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1262 1263 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1264 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 1265 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 1266 try std.testing.expect(user_event.removed); 1267 1268 // removing the user removes their active repos too 1269 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1270 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1271 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1272 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1273 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1274 try std.testing.expect(repo_event.removed); 1275 } 1276 } 1277 1278 test "repos and users paginate newest first" { 1279 const io = std.testing.io; 1280 const allocator = std.testing.allocator; 1281 const temp_dir_name = "temp-event-order"; 1282 1283 const cwd = std.Io.Dir.cwd(); 1284 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1285 if (temp_dir_or_err) |*temp_dir| { 1286 temp_dir.close(io); 1287 try cwd.deleteTree(io, temp_dir_name); 1288 } else |_| {} 1289 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1290 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1291 defer temp_dir.close(io); 1292 1293 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1294 defer allocator.free(cwd_path); 1295 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 1296 defer allocator.free(work_path); 1297 1298 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 1299 const Repo = rp.Repo(.xit, repo_opts); 1300 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 1301 defer repo.deinit(io, allocator); 1302 1303 var arena = std.heap.ArenaAllocator.init(allocator); 1304 defer arena.deinit(); 1305 1306 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1307 const user_id = evt.EventWithId.randomId(prng.random()); 1308 var repo_ids: [4][evt.event_id_size]u8 = undefined; 1309 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 1310 1311 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 1312 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 1313 1314 // asserts the repo-id-set holds exactly `expected` ids in that order 1315 const Check = struct { 1316 fn order( 1317 comptime DB: type, 1318 comptime hash_kind: hash.HashKind, 1319 moment: DB.HashMap(.read_only), 1320 expected: []const *const [evt.event_id_size]u8, 1321 ) !void { 1322 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 1323 const set = try DB.SortedSet(.read_only).init(cursor); 1324 try std.testing.expectEqual(expected.len, try set.count()); 1325 for (expected, 0..) |id, i| { 1326 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 1327 // the key is orderKeyDesc ([timestamp][event-id]); its trailing bytes are the id 1328 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 1329 _ = try kv.key_cursor.readBytes(&order_key); 1330 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 1331 } 1332 } 1333 }; 1334 1335 // one user, then four repos, each with its own creation timestamp (user@100, 1336 // repos@101..104) 1337 const events = [_]evt.EventWithId{ 1338 .{ .id = std.fmt.bytesToHex(user_id, .lower), .author = author, .timestamp = 100, .event = .{ .user = .{ .name = "alice", .email = "alice@example.test", .password_hash = pw } } }, 1339 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 101, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0" } } }, 1340 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 102, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1" } } }, 1341 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .author = author, .timestamp = 103, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2" } } }, 1342 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .author = author, .timestamp = 104, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3" } } }, 1343 }; 1344 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events); 1345 1346 { 1347 const moment = try evt.currentMoment(repo_opts, &repo); 1348 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 1349 1350 // the single user shows up in its own ordered set 1351 const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 1352 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 1353 try std.testing.expectEqual(1, try uset.count()); 1354 } 1355 1356 // removing repo1 retains its place in the canonical order 1357 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 1358 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author = author, .timestamp = 200, .event = .{ .repo = null } }, 1359 }); 1360 { 1361 const moment = try evt.currentMoment(repo_opts, &repo); 1362 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 1363 } 1364 1365 // update repo0 at a later timestamp -> keeps its original place 1366 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 1367 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author = author, .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated" } } }, 1368 }); 1369 { 1370 const moment = try evt.currentMoment(repo_opts, &repo); 1371 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 1372 1373 // the value really was updated 1374 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1375 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 1376 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 1377 const rm = try Repo.DB.HashMap(.read_only).init(rc); 1378 const re = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, rm); 1379 try std.testing.expectEqualStrings("updated", re.event.description); 1380 } 1381 } 1382