╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
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_email = author_email, 1103 .event = .{ 1104 .user = .{ 1105 .name = "alice", 1106 .display_name = "Alice Example", 1107 .email = "alice@example.test", 1108 .password_hash = first_password_hash, 1109 }, 1110 }, 1111 }, 1112 // this event edits the previous one because it has the same id 1113 .{ 1114 .id = std.fmt.bytesToHex(user_event_id, .lower), 1115 .author_email = author_email, 1116 .event = .{ 1117 .user = .{ 1118 .name = "alice", 1119 .display_name = "Alice Example", 1120 .email = "alice@example.test", 1121 .password_hash = second_password_hash, 1122 }, 1123 }, 1124 }, 1125 .{ 1126 .id = std.fmt.bytesToHex(repo_event_id, .lower), 1127 .author_email = author_email, 1128 .event = .{ 1129 .repo = .{ 1130 .user_id = &user_event_id, 1131 .name = "ziglings", 1132 .description = "Learn the Zig programming language by fixing tiny broken programs", 1133 }, 1134 }, 1135 }, 1136 }; 1137 1138 // commit and consume the seed events 1139 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 1140 1141 { 1142 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1143 1144 // get the map of users 1145 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1146 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1147 1148 // get the user out of the map that was edited 1149 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1150 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 1151 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 1152 1153 // the password was correctly edited 1154 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.event.password_hash); 1155 1156 // get the map of repos 1157 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1158 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1159 1160 // get the repo out of the map 1161 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1162 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1163 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1164 1165 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.event.user_id); 1166 try std.testing.expectEqualStrings("ziglings", repo_event.event.name); 1167 1168 // get the repos created by the user 1169 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; 1170 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 1171 1172 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1173 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 1174 1175 try std.testing.expectEqual(1, try user_repos.count()); 1176 1177 // the set is keyed by orderKeyDesc([created-ts][event-id]) 1178 const order_key = evt.orderKeyDesc(repo_event.created_ts, &repo_event_id); 1179 try std.testing.expect(try user_repos.contains(&order_key)); 1180 } 1181 1182 // 1183 // remove the repo 1184 // 1185 1186 const events_to_consume2 = [_]evt.EventWithId{ 1187 .{ 1188 .id = std.fmt.bytesToHex(repo_event_id, .lower), 1189 .author_email = author_email, 1190 .event = .{ .repo = null }, 1191 }, 1192 }; 1193 1194 // commit and consume the removal 1195 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume2); 1196 1197 { 1198 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1199 1200 // get the map of repos 1201 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1202 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1203 1204 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1205 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1206 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1207 try std.testing.expect(repo_event.deleted); 1208 1209 // get the repos created by the user 1210 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; 1211 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 1212 1213 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1214 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 1215 1216 // removing the repo emptied the user's set 1217 try std.testing.expectEqual(0, try user_repos.count()); 1218 } 1219 1220 // a non-null payload restores the repo and its indexes 1221 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{.{ 1222 .id = std.fmt.bytesToHex(repo_event_id, .lower), 1223 .author_email = author_email, 1224 .event = events_to_consume[2].event, 1225 }}); 1226 1227 { 1228 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1229 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1230 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1231 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1232 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1233 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1234 try std.testing.expect(!repo_event.deleted); 1235 1236 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; 1237 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 1238 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1239 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 1240 try std.testing.expectEqual(1, try user_repos.count()); 1241 } 1242 1243 // 1244 // remove the user 1245 // 1246 1247 const events_to_consume3 = [_]evt.EventWithId{ 1248 .{ 1249 .id = std.fmt.bytesToHex(user_event_id, .lower), 1250 .author_email = author_email, 1251 .event = .{ .user = null }, 1252 }, 1253 }; 1254 1255 // commit and consume the removal 1256 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 1257 1258 { 1259 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 1260 1261 // get the map of users 1262 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 1263 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 1264 1265 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 1266 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 1267 const user_event = try evt.read(evt.User.Record, Repo.DB, repo_opts.hash, &arena, user_map); 1268 try std.testing.expect(user_event.deleted); 1269 1270 // deleting the user tombstones their active repos too 1271 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1272 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 1273 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 1274 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 1275 const repo_event = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, repo_map); 1276 try std.testing.expect(repo_event.deleted); 1277 } 1278 } 1279 1280 test "repos and users paginate newest first" { 1281 const io = std.testing.io; 1282 const allocator = std.testing.allocator; 1283 const temp_dir_name = "temp-event-order"; 1284 1285 const cwd = std.Io.Dir.cwd(); 1286 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 1287 if (temp_dir_or_err) |*temp_dir| { 1288 temp_dir.close(io); 1289 try cwd.deleteTree(io, temp_dir_name); 1290 } else |_| {} 1291 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 1292 defer cwd.deleteTree(io, temp_dir_name) catch {}; 1293 defer temp_dir.close(io); 1294 1295 const cwd_path = try std.process.currentPathAlloc(io, allocator); 1296 defer allocator.free(cwd_path); 1297 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 1298 defer allocator.free(work_path); 1299 1300 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 1301 const Repo = rp.Repo(.xit, repo_opts); 1302 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 1303 defer repo.deinit(io, allocator); 1304 1305 var arena = std.heap.ArenaAllocator.init(allocator); 1306 defer arena.deinit(); 1307 1308 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 1309 const user_id = evt.EventWithId.randomId(prng.random()); 1310 var repo_ids: [4][evt.event_id_size]u8 = undefined; 1311 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 1312 1313 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 1314 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 1315 1316 // asserts the repo-id-set holds exactly `expected` ids in that order 1317 const Check = struct { 1318 fn order( 1319 comptime DB: type, 1320 comptime hash_kind: hash.HashKind, 1321 moment: DB.HashMap(.read_only), 1322 expected: []const *const [evt.event_id_size]u8, 1323 ) !void { 1324 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 1325 const set = try DB.SortedSet(.read_only).init(cursor); 1326 try std.testing.expectEqual(expected.len, try set.count()); 1327 for (expected, 0..) |id, i| { 1328 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 1329 // the key is orderKeyDesc ([timestamp][event-id]); its trailing bytes are the id 1330 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 1331 _ = try kv.key_cursor.readBytes(&order_key); 1332 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 1333 } 1334 } 1335 }; 1336 1337 // one user, then four repos, each with its own creation timestamp (user@100, 1338 // repos@101..104) 1339 const events = [_]evt.EventWithId{ 1340 .{ .id = std.fmt.bytesToHex(user_id, .lower), .author_email = author_email, .timestamp = 100, .event = .{ .user = .{ .name = "alice", .display_name = "Alice", .email = "alice@example.test", .password_hash = pw } } }, 1341 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author_email = author_email, .timestamp = 101, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0" } } }, 1342 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author_email = author_email, .timestamp = 102, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1" } } }, 1343 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .author_email = author_email, .timestamp = 103, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2" } } }, 1344 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .author_email = author_email, .timestamp = 104, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3" } } }, 1345 }; 1346 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &events); 1347 1348 { 1349 const moment = try evt.currentMoment(repo_opts, &repo); 1350 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 1351 1352 // the single user shows up in its own ordered set 1353 const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 1354 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 1355 try std.testing.expectEqual(1, try uset.count()); 1356 } 1357 1358 // deleting repo1 retains its place in the canonical order 1359 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 1360 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .author_email = author_email, .timestamp = 200, .event = .{ .repo = null } }, 1361 }); 1362 { 1363 const moment = try evt.currentMoment(repo_opts, &repo); 1364 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 1365 } 1366 1367 // update repo0 at a later timestamp -> keeps its original place 1368 try evt.consume(.xit, repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 1369 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .author_email = author_email, .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated" } } }, 1370 }); 1371 { 1372 const moment = try evt.currentMoment(repo_opts, &repo); 1373 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[3], &repo_ids[2], &repo_ids[1], &repo_ids[0] }); 1374 1375 // the value really was updated 1376 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 1377 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 1378 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 1379 const rm = try Repo.DB.HashMap(.read_only).init(rc); 1380 const re = try evt.read(evt.Repo.Record, Repo.DB, repo_opts.hash, &arena, rm); 1381 try std.testing.expectEqualStrings("updated", re.event.description); 1382 } 1383 } 1384