╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ event.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
718 .display_name = "Alice Example", 719 .email = "alice@example.test", 720 .password_hash = second_password_hash, 721 }, 722 }, 723 }, 724 .{ 725 .id = std.fmt.bytesToHex(repo_event_id, .lower), 726 .event = .{ 727 .repo = .{ 728 .user_id = &user_event_id, 729 .name = "ziglings", 730 .description = "Learn the Zig programming language by fixing tiny broken programs", 731 .enable_issue = true, 732 }, 733 }, 734 }, 735 }; 736 737 // commit and consume the seed events 738 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume); 739 740 { 741 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 742 743 // get the map of users 744 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 745 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 746 747 // get the user out of the map that was edited 748 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 749 const user_map = try Repo.DB.HashMap(.read_only).init(user_cursor); 750 const user_event = try evt.read(evt.User, Repo.DB, repo_opts.hash, &arena, user_map); 751 752 // the password was correctly edited 753 try std.testing.expectEqualStrings(events_to_consume[1].event.user.?.password_hash, user_event.password_hash); 754 755 // get the map of repos 756 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 757 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 758 759 // get the repo out of the map 760 const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id)) orelse return error.NotFound; 761 const repo_map = try Repo.DB.HashMap(.read_only).init(repo_cursor); 762 const repo_event = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, repo_map); 763 764 try std.testing.expectEqualSlices(u8, &user_event_id, repo_event.user_id); 765 try std.testing.expectEqualStrings("ziglings", repo_event.name); 766 767 // get the repos created by the user 768 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; 769 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 770 771 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 772 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 773 774 try std.testing.expectEqual(1, try user_repos.count()); 775 776 // the set is keyed by orderKey([created-ts][event-id]) 777 const order_key = evt.orderKey(repo_event.created_ts, &repo_event_id); 778 try std.testing.expect(try user_repos.contains(&order_key)); 779 } 780 781 // 782 // remove the repo 783 // 784 785 const events_to_consume2 = [_]evt.EventWithId{ 786 .{ 787 .id = std.fmt.bytesToHex(repo_event_id, .lower), 788 .event = .{ .repo = null }, 789 }, 790 }; 791 792 // commit and consume the removal 793 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume2); 794 795 { 796 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 797 798 // get the map of repos 799 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 800 const event_id_to_repo = try Repo.DB.HashMap(.read_only).init(event_id_to_repo_cursor); 801 802 try std.testing.expect(null == try event_id_to_repo.getCursor(hash.hashInt(repo_opts.hash, &repo_event_id))); 803 804 // get the repos created by the user 805 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; 806 const user_id_to_repo_id_set = try Repo.DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); 807 808 const user_repos_cursor = try user_id_to_repo_id_set.getCursor(hash.hashInt(repo_opts.hash, &user_event_id)) orelse return error.NotFound; 809 const user_repos = try Repo.DB.SortedSet(.read_only).init(user_repos_cursor); 810 811 // removing the repo emptied the user's set 812 try std.testing.expectEqual(0, try user_repos.count()); 813 } 814 815 // 816 // remove the user 817 // 818 819 const events_to_consume3 = [_]evt.EventWithId{ 820 .{ 821 .id = std.fmt.bytesToHex(user_event_id, .lower), 822 .event = .{ .user = null }, 823 }, 824 }; 825 826 // commit and consume the removal 827 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events_to_consume3); 828 829 { 830 const haxy_moment = try evt.currentMoment(repo_opts, &repo); 831 832 // get the map of users 833 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->user")) orelse return error.NotFound; 834 const event_id_to_user = try Repo.DB.HashMap(.read_only).init(event_id_to_user_cursor); 835 836 try std.testing.expect(null == try event_id_to_user.getCursor(hash.hashInt(repo_opts.hash, &user_event_id))); 837 } 838 } 839 840 test "repos and users paginate in creation order" { 841 const io = std.testing.io; 842 const allocator = std.testing.allocator; 843 const temp_dir_name = "temp-event-order"; 844 845 const cwd = std.Io.Dir.cwd(); 846 var temp_dir_or_err = cwd.openDir(io, temp_dir_name, .{}); 847 if (temp_dir_or_err) |*temp_dir| { 848 temp_dir.close(io); 849 try cwd.deleteTree(io, temp_dir_name); 850 } else |_| {} 851 var temp_dir = try cwd.createDirPathOpen(io, temp_dir_name, .{}); 852 defer cwd.deleteTree(io, temp_dir_name) catch {}; 853 defer temp_dir.close(io); 854 855 const cwd_path = try std.process.currentPathAlloc(io, allocator); 856 defer allocator.free(cwd_path); 857 const work_path = try std.fs.path.join(allocator, &.{ cwd_path, temp_dir_name }); 858 defer allocator.free(work_path); 859 860 const repo_opts: rp.RepoOpts(.xit) = .{ .is_test = true }; 861 const Repo = rp.Repo(.xit, repo_opts); 862 var repo = try Repo.init(io, allocator, .{ .path = work_path }); 863 defer repo.deinit(io, allocator); 864 865 var arena = std.heap.ArenaAllocator.init(allocator); 866 defer arena.deinit(); 867 868 var prng = std.Random.DefaultPrng.init(std.testing.random_seed); 869 const user_id = evt.EventWithId.randomId(prng.random()); 870 var repo_ids: [4][evt.event_id_size]u8 = undefined; 871 for (&repo_ids) |*id| id.* = evt.EventWithId.randomId(prng.random()); 872 873 var pw_buf: [evt.User.password_hash_max_len]u8 = undefined; 874 const pw = try evt.User.hashPassword("pw", &pw_buf, io); 875 876 // asserts the repo-id-set holds exactly `expected` ids in that order 877 const Check = struct { 878 fn order( 879 comptime DB: type, 880 comptime hash_kind: hash.HashKind, 881 moment: DB.HashMap(.read_only), 882 expected: []const *const [evt.event_id_size]u8, 883 ) !void { 884 const cursor = try moment.getCursor(hash.hashInt(hash_kind, "repo-id-set")) orelse return error.NotFound; 885 const set = try DB.SortedSet(.read_only).init(cursor); 886 try std.testing.expectEqual(expected.len, try set.count()); 887 for (expected, 0..) |id, i| { 888 const kv = (try set.getIndexKeyValuePair(@intCast(i))) orelse return error.NotFound; 889 // the key is orderKey ([timestamp][event-id]); its trailing bytes are the id 890 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 891 _ = try kv.key_cursor.readBytes(&order_key); 892 try std.testing.expectEqualSlices(u8, id, order_key[@sizeOf(u64)..]); 893 } 894 } 895 }; 896 897 // one user, then four repos, each with its own creation timestamp (user@100, 898 // repos@101..104) 899 const events = [_]evt.EventWithId{ 900 .{ .id = std.fmt.bytesToHex(user_id, .lower), .timestamp = 100, .event = .{ .user = .{ .name = "alice", .display_name = "Alice", .email = "alice@example.test", .password_hash = pw } } }, 901 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .timestamp = 101, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "d0", .enable_issue = true } } }, 902 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .timestamp = 102, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo1", .description = "d1", .enable_issue = true } } }, 903 .{ .id = std.fmt.bytesToHex(repo_ids[2], .lower), .timestamp = 103, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo2", .description = "d2", .enable_issue = true } } }, 904 .{ .id = std.fmt.bytesToHex(repo_ids[3], .lower), .timestamp = 104, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo3", .description = "d3", .enable_issue = true } } }, 905 }; 906 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &events); 907 908 { 909 const moment = try evt.currentMoment(repo_opts, &repo); 910 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[1], &repo_ids[2], &repo_ids[3] }); 911 912 // the single user shows up in its own ordered set 913 const ucur = try moment.getCursor(hash.hashInt(repo_opts.hash, "user-id-set")) orelse return error.NotFound; 914 const uset = try Repo.DB.SortedSet(.read_only).init(ucur); 915 try std.testing.expectEqual(1, try uset.count()); 916 } 917 918 // delete repo1 -> dense order (no tombstone) 919 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 920 .{ .id = std.fmt.bytesToHex(repo_ids[1], .lower), .timestamp = 200, .event = .{ .repo = null } }, 921 }); 922 { 923 const moment = try evt.currentMoment(repo_opts, &repo); 924 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[2], &repo_ids[3] }); 925 } 926 927 // update repo0 at a later timestamp -> keeps its original slot 928 try evt.commitAndConsume(repo_opts, io, allocator, &repo, evt.events_ref, &[_]evt.EventWithId{ 929 .{ .id = std.fmt.bytesToHex(repo_ids[0], .lower), .timestamp = 300, .event = .{ .repo = .{ .user_id = &user_id, .name = "repo0", .description = "updated", .enable_issue = true } } }, 930 }); 931 { 932 const moment = try evt.currentMoment(repo_opts, &repo); 933 try Check.order(Repo.DB, repo_opts.hash, moment, &.{ &repo_ids[0], &repo_ids[2], &repo_ids[3] }); 934 935 // the value really was updated 936 const e2r_cur = try moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->repo")) orelse return error.NotFound; 937 const e2r = try Repo.DB.HashMap(.read_only).init(e2r_cur); 938 const rc = try e2r.getCursor(hash.hashInt(repo_opts.hash, &repo_ids[0])) orelse return error.NotFound; 939 const rm = try Repo.DB.HashMap(.read_only).init(rc); 940 const re = try evt.read(evt.Repo, Repo.DB, repo_opts.hash, &arena, rm); 941 try std.testing.expectEqualStrings("updated", re.description); 942 } 943 } 944