╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ notify.zig ───────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
454 const Inboxes = struct { 455 arena: *std.heap.ArenaAllocator, 456 admin_moment: evt.AdminDB.HashMap(.read_only), 457 owner_moment: UserDB.HashMap(.read_only), 458 repo: evt.Repo.Record, 459 repo_id: *const [evt.event_id_size]u8, 460 // whether each user is active and can read the repo 461 readers: std.AutoHashMapUnmanaged([evt.event_id_size]u8, bool) = .empty, 462 by_user: std.AutoArrayHashMapUnmanaged([evt.event_id_size]u8, std.ArrayList(Notif)) = .empty, 463 464 fn add(self: *Inboxes, user_id: [evt.event_id_size]u8, notif: Notif) !void { 465 if (!try self.canRead(user_id)) return; 466 const entry = try self.by_user.getOrPut(self.arena.allocator(), user_id); 467 if (!entry.found_existing) entry.value_ptr.* = .empty; 468 // events arrive newest first, so a thread keeps its latest child 469 for (entry.value_ptr.items) |existing| { 470 if (std.mem.eql(u8, &existing.thread_id, &notif.thread_id)) return; 471 } 472 try entry.value_ptr.append(self.arena.allocator(), notif); 473 } 474 475 fn canRead(self: *Inboxes, user_id: [evt.event_id_size]u8) !bool { 476 if (self.readers.get(user_id)) |can_read| return can_read; 477 const can_read = if (try evt.User.readById(evt.AdminDB, evt.admin_repo_opts.hash, self.admin_moment, self.arena, &user_id)) |user| 478 !user.removed and try evt.Repo.roleOf(UserDB, hash_kind, self.owner_moment, self.arena, self.repo, self.repo_id, user_id) != null 479 else 480 false; 481 try self.readers.put(self.arena.allocator(), user_id, can_read); 482 return can_read; 483 } 484 }; 485 486 // deliver the comments and assignments created since the repo's cursor, then advance it 487 fn notifyRepoEvents( 488 comptime repo_opts: rp.RepoOpts(.xit), 489 io: std.Io, 490 allocator: std.mem.Allocator, 491 users_dir: []const u8, 492 admin_repo_path: []const u8, 493 location: evt.RepoLocation, 494 repo: *rp.Repo(.xit, repo_opts), 495 ) !void { 496 const DB = evt.EventDB(repo_opts.hash); 497 const haxy_moment = evt.currentMoment(repo_opts, repo) catch |err| switch (err) { 498 error.NotFound => return, 499 else => |e| return e, 500 }; 501 const index_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.moment_index_key)) orelse return error.CursorNotFound; 502 const current = try index_cursor.readUint(); 503 504 var owner_repo = (try evt.openUserRepo(io, allocator, users_dir, &location.owner_id)) orelse return; 505 defer owner_repo.deinit(io, allocator); 506 507 // a missing cursor starts from the repo's first transaction, and a rewound history resets it 508 const first = if (try readLastNotified(&owner_repo, &location.repo_id)) |last| blk: { 509 if (current < last) return writeLastNotified(io, &owner_repo, &location.repo_id, current); 510 if (current == last) return; 511 break :blk last + 1; 512 } else 0; 513 514 var arena = std.heap.ArenaAllocator.init(allocator); 515 defer arena.deinit(); 516 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; 523 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); 526 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 }; 537 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); 540 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); 554 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 }; 567 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; 577 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 }; 587 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 } 596 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 }; 604 } 605 if (failed) return error.DeliveryFailed; 606 } 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 } 617 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 }); 631 } 632