╭ changed ─────╮ │src/notify.zig│ ╰──────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭──────────╮ │← previous│ ╰──────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │517 │ │517 - deliver: { │ │518 - const owner_moment = (try evt.userMoment(&owner_repo)) orelse break :deliver; │ │519 - // the owner's plain data, which holds the subscriber index │ │520 - const owner_latest = try owner_repo.core.latestMoment(); │ │521 - const repo_record = (try evt.Repo.readById(UserDB, hash_kind, owner_moment, &arena, &location.repo_id)) orelse break :deliver; │ │522 - if (repo_record.removed) break :deliver; │ │518 + const repo_record = (try evt.Repo.readById(UserDB, hash_kind, owner_moment, &arena, &location.repo_id)) orelse return; │ │519 + if (repo_record.removed) return; │ │520 │ │524 - var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }); │ │525 - defer admin_repo.deinit(io, allocator); │ │521 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = admin_repo_path }); │ │522 + defer admin_repo.deinit(io, allocator); │ │523 │ │527 - const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │528 - const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &location.owner_id)) orelse break :deliver; │ │529 - │ │530 - var inboxes: Inboxes = .{ │ │531 - .arena = &arena, │ │532 - .admin_moment = admin_moment, │ │533 - .owner_moment = owner_moment, │ │534 - .repo = repo_record, │ │535 - .repo_id = &location.repo_id, │ │536 - }; │ │524 + const admin_moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │525 + const owner = (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, &arena, &location.owner_id)) orelse return; │ │526 │ │538 - const kinds_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.event_index_key)) orelse return error.CursorNotFound; │ │539 - const kinds = try DB.HashMap(.read_only).init(kinds_cursor); │ │527 + var inboxes: Inboxes = .{ │ │528 + .arena = &arena, │ │529 + .admin_moment = admin_moment, │ │530 + .owner_moment = owner_moment, │ │531 + .repo = repo_record, │ │532 + .repo_id = &location.repo_id, │ │533 + }; │ │534 │ │541 - // the events each transaction since the cursor consumed, newest first │ │542 - var index = current + 1; │ │543 - while (index > first) { │ │544 - index -= 1; │ │545 - const ids = (try evt.momentEventIds(DB, repo_opts.hash, haxy_moment, index)) orelse continue; │ │546 - var iter = try ids.iteratorFromIndex(0); │ │547 - while (try iter.next()) |kv_cursor| { │ │548 - var cursor = kv_cursor; │ │549 - const kv_pair = try cursor.readKeyValuePair(); │ │550 - var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │551 - _ = try kv_pair.key_cursor.readBytes(&order_key); │ │552 - const id = order_key[@sizeOf(u64)..].*; │ │553 - const order = std.math.maxInt(u64) - std.mem.readInt(u64, order_key[0..@sizeOf(u64)], .big); │ │535 + const kinds_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.event_index_key)) orelse return error.CursorNotFound; │ │536 + const kinds = try DB.HashMap(.read_only).init(kinds_cursor); │ │537 │ │555 - // edits keep the created order, so only creations notify │ │556 - const Created = struct { thread_id: [evt.event_id_size * 2]u8, actor_email: ?[]const u8 }; │ │557 - const created: Created = switch ((try evt.readEventKind(repo_opts.hash, kinds, &id)) orelse continue) { │ │558 - inline .comment, .assign => |kind| blk: { │ │559 - const Fields = struct { removed: bool, author_email: ?[]const u8, created_order: u64, event: struct { thread_id: [evt.event_id_size * 2]u8 } };│ │560 - const T = if (kind == .comment) evt.Comment else evt.Assignment; │ │561 - const record = (try evt.readRecordSubset(T, Fields, DB, repo_opts.hash, haxy_moment, &arena, &id)) orelse continue; │ │562 - if (record.removed or record.created_order != order) continue; │ │563 - break :blk .{ .thread_id = record.event.thread_id, .actor_email = record.author_email }; │ │564 - }, │ │565 - else => continue, │ │566 - }; │ │538 + var iter = try ids.iteratorFromIndex(0); │ │539 + while (try iter.next()) |kv_cursor| { │ │540 + var cursor = kv_cursor; │ │541 + const kv_pair = try cursor.readKeyValuePair(); │ │542 + var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │543 + _ = try kv_pair.key_cursor.readBytes(&order_key); │ │544 + const id = order_key[@sizeOf(u64)..].*; │ │545 + const order = std.math.maxInt(u64) - std.mem.readInt(u64, order_key[0..@sizeOf(u64)], .big); │ │546 │ │568 - const thread_id = try evt.parseEventId(&created.thread_id); │ │569 - const Titled = struct { event: struct { title: []const u8 } }; │ │570 - const thread_kind = (try evt.readEventKind(repo_opts.hash, kinds, &thread_id)) orelse continue; │ │571 - const thread = (switch (thread_kind) { │ │572 - .issue => try evt.readRecordSubset(evt.Issue, Titled, DB, repo_opts.hash, haxy_moment, &arena, &thread_id), │ │573 - .patch => try evt.readRecordSubset(evt.Patch, Titled, DB, repo_opts.hash, haxy_moment, &arena, &thread_id), │ │574 - .discuss => try evt.readRecordSubset(evt.Discussion, Titled, DB, repo_opts.hash, haxy_moment, &arena, &thread_id), │ │575 - else => null, │ │576 - }) orelse continue; │ │547 + // edits keep the created order, so only creations notify │ │548 + const Created = struct { thread_id: [evt.event_id_size * 2]u8, actor_email: ?[]const u8 }; │ │549 + const created: Created = switch ((try evt.readEventKind(repo_opts.hash, kinds, &id)) orelse continue) { │ │550 + inline .comment, .assign => |kind| blk: { │ │551 + const Fields = struct { removed: bool, author_email: ?[]const u8, created_order: u64, event: struct { thread_id: [evt.event_id_size * 2]u8 } }; │ │552 + const T = if (kind == .comment) evt.Comment else evt.Assignment; │ │553 + const record = (try evt.readRecordSubset(T, Fields, DB, repo_opts.hash, haxy_moment, &arena, &id)) orelse continue; │ │554 + if (record.removed or record.created_order != order) continue; │ │555 + break :blk .{ .thread_id = record.event.thread_id, .actor_email = record.author_email }; │ │556 + }, │ │557 + else => continue, │ │558 + }; │ │559 │ │578 - const notif: Notif = .{ │ │579 - .repo_id = location.repo_id, │ │580 - .owner_name = owner.event.name, │ │581 - .repo_name = repo_record.event.name, │ │582 - .thread_id = thread_id, │ │583 - .thread_kind = thread_kind, │ │584 - .title = thread.event.title, │ │585 - .last_child_id = id, │ │586 - }; │ │560 + const thread_id = try evt.parseEventId(&created.thread_id); │ │561 + const Titled = struct { event: struct { title: []const u8 } }; │ │562 + const thread_kind = (try evt.readEventKind(repo_opts.hash, kinds, &thread_id)) orelse continue; │ │563 + const thread = (switch (thread_kind) { │ │564 + .issue => try evt.readRecordSubset(evt.Issue, Titled, DB, repo_opts.hash, haxy_moment, &arena, &thread_id), │ │565 + .patch => try evt.readRecordSubset(evt.Patch, Titled, DB, repo_opts.hash, haxy_moment, &arena, &thread_id), │ │566 + .discuss => try evt.readRecordSubset(evt.Discussion, Titled, DB, repo_opts.hash, haxy_moment, &arena, &thread_id), │ │567 + else => null, │ │568 + }) orelse continue; │ │569 │ │588 - // subscribers hear of every comment and assignment but their own │ │589 - const actor_id = if (created.actor_email) |email| try evt.User.readIdByEmail(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, email) else null; │ │590 - for (try readSubscribers(owner_latest, &arena, &idOf(&location.repo_id, &thread_id))) |user_id| { │ │591 - if (actor_id) |actor| if (std.mem.eql(u8, &actor, &user_id)) continue; │ │592 - try inboxes.add(user_id, notif); │ │593 - } │ │594 - } │ │595 - } │ │570 + const notif: Notif = .{ │ │571 + .repo_id = location.repo_id, │ │572 + .owner_name = owner.event.name, │ │573 + .repo_name = repo_record.event.name, │ │574 + .thread_id = thread_id, │ │575 + .thread_kind = thread_kind, │ │576 + .title = thread.event.title, │ │577 + .last_child_id = id, │ │578 + }; │ │579 │ │597 - // a failure leaves the cursor, so the repo's next round redelivers │ │598 - var failed = false; │ │599 - for (inboxes.by_user.keys(), inboxes.by_user.values()) |*user_id, notifs| { │ │600 - deliver(io, allocator, users_dir, user_id, notifs.items) catch |err| { │ │601 - std.log.warn("failed to deliver notifications: {s}", .{@errorName(err)}); │ │602 - failed = true; │ │603 - }; │ │580 + // a user hears of no comment or assignment they made or pushed │ │581 + const actor_id = if (created.actor_email) |email| try evt.User.readIdByEmail(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, email) else null; │ │582 + for (try readNotified(owner_latest, &arena, &idOf(&location.repo_id, &thread_id))) |user_id| { │ │583 + if (actor_id) |actor| if (std.mem.eql(u8, &actor, &user_id)) continue; │ │584 + if (pusher_id) |pusher| if (std.mem.eql(u8, pusher, &user_id)) continue; │ │585 + try inboxes.add(user_id, notif); │ │586 } │ │605 - if (failed) return error.DeliveryFailed; │ │587 } │ │607 - │ │608 - try writeLastNotified(io, &owner_repo, &location.repo_id, current); │ │609 -} │ │610 - │ │611 -fn readLastNotified(owner_repo: *rp.Repo(.xit, evt.user_repo_opts), repo_id: *const [evt.event_id_size]u8) !?u64 { │ │612 - const map_cursor = try (try owner_repo.core.latestMoment()).getCursor(hash.hashInt(hash_kind, last_notified_map_key)) orelse return null; │ │613 - const map = try UserDB.HashMap(.read_only).init(map_cursor); │ │614 - const cursor = try map.getCursor(hash.hashInt(hash_kind, repo_id)) orelse return null; │ │615 - return try cursor.readUint(); │ │616 -} │ │588 │ │618 -fn writeLastNotified(io: std.Io, owner_repo: *rp.Repo(.xit, evt.user_repo_opts), repo_id: *const [evt.event_id_size]u8, moment_index: u64) !void { │ │619 - const Ctx = struct { │ │620 - repo_id: *const [evt.event_id_size]u8, │ │621 - moment_index: u64, │ │622 - │ │623 - pub fn run(ctx: @This(), cursor: *UserDB.Cursor(.read_write)) !void { │ │624 - const moment = try UserDB.HashMap(.read_write).init(cursor.*); │ │625 - const map = try UserDB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(hash_kind, last_notified_map_key))); │ │626 - try map.put(hash.hashInt(hash_kind, ctx.repo_id), .{ .uint = ctx.moment_index }); │ │627 - } │ │628 - }; │ │629 - │ │630 - try transactIn(io, owner_repo, Ctx{ .repo_id = repo_id, .moment_index = moment_index }); │ │589 + if (inboxes.by_user.count() == 0) return; │ │590 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .text = "Delivering notifications" }); │ │591 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .start = .{ .kind = .writing_patch, .estimated_total_items = inboxes.by_user.count() } }); │ │592 + for (inboxes.by_user.keys(), inboxes.by_user.values()) |*user_id, notifs| { │ │593 + deliver(io, allocator, users_dir, user_id, notifs.items) catch |err| { │ │594 + std.log.warn("failed to deliver notifications: {s}", .{@errorName(err)}); │ │595 + }; │ │596 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .complete_one = .writing_patch }); │ │597 + } │ │598 + progress.report(repo_opts, io, progress_ctx_maybe, .{ .end = .writing_patch }); │ │599 } │ │600 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯