╭ changed ─────╮ │src/notify.zig│ ╰──────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────╮ │3 const rp = xit.repo; │ │4 const hash = xit.hash; │ │5 const evt = @import("event.zig"); │ │6 +const progress = @import("progress.zig"); │ │7 │ │8 const UserDB = evt.UserDB; │ │9 const hash_kind = evt.user_repo_opts.hash;│ ╰─────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────╮ │13 const recent_notif_id_set_key = "haxy/recent-notif-id-set"; │ │14 const unread_notif_id_set_key = "haxy/unread-notif-id-set"; │ │15 │ │15 -// the owner's user repo key holding each repo's newest moment index already notified │ │16 -const last_notified_map_key = "haxy/repo-id->last-notified-moment-index"; │ │16 +// the most notifs a user keeps │ │17 +pub const max_notifs = 1000; │ │18 │ │18 -// the owner's user repo key mapping each thread's notif id to its subscribers' user ids│ │19 -const subscriber_map_key = "haxy/notif-id->subscriber-id-set"; │ │19 +// the most users a thread's capped tiers hold │ │20 +pub const max_commented = 250; │ │21 +pub const max_subscribed = 100; │ │22 │ │21 -// the most notifs a user keeps │ │22 -pub const max_notifs = 1000; │ │23 +// the owner's user repo key mapping each thread's notif id to the user ids in │ │24 +// `mode`'s tier, and the most that tier holds. a mute has no tier. │ │25 +fn tierOf(comptime mode: evt.Notify.Mode) ?struct { map_key: []const u8, cap: ?u64 } { │ │26 + const map_key = "haxy/notif-id->" ++ @tagName(mode) ++ "-id-set"; │ │27 + return switch (mode) { │ │28 + .involved => .{ .map_key = map_key, .cap = null }, │ │29 + .commented => .{ .map_key = map_key, .cap = max_commented }, │ │30 + .subscribed => .{ .map_key = map_key, .cap = max_subscribed }, │ │31 + .muted => null, │ │32 + }; │ │33 +} │ │34 │ │35 // one thread's item in a user's inbox. the names and title are as of its last bump. │ │36 pub const Notif = struct { │ ╰────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │67 fn transact(io: std.Io, allocator: std.mem.Allocator, users_dir: []const u8, user_id: *const [evt.event_id_size]u8, ctx: anytype) !void {│ │68 var user_repo = (try evt.openUserRepo(io, allocator, users_dir, user_id)) orelse return error.NotFound; │ │69 defer user_repo.deinit(io, allocator); │ │59 - try transactIn(io, &user_repo, ctx); │ │60 -} │ │70 │ │62 -// run `ctx` in a transaction on an open user repo │ │63 -fn transactIn(io: std.Io, user_repo: *rp.Repo(.xit, evt.user_repo_opts), ctx: anytype) !void { │ │71 try user_repo.core.db_file.lock(io, .exclusive); │ │72 defer user_repo.core.db_file.unlock(io); │ │73 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │234 return try (try UserDB.SortedSet(.read_only).init(unread_cursor)).count();│ │235 } │ │236 │ │230 -// always notify `user_id` of the thread `thread_id` in the repo at `location`│ │237 +// notify `user_id` of the thread `thread_id` in the repo at `location` by │ │238 +// choice, unless a tier already holds them or the subscribed tier is full │ │239 pub fn subscribe( │ │240 io: std.Io, │ │241 allocator: std.mem.Allocator, │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │245 thread_id: *const [evt.event_id_size]u8, │ │246 author: evt.CommitAuthor, │ │247 ) !void { │ │240 - try writeMode(io, allocator, users_dir, user_id, &location, thread_id, .subscribe, author); │ │241 - indexSubscriber(io, allocator, users_dir, location, thread_id, user_id, true); │ │248 + { │ │249 + var owner_repo = (try evt.openUserRepo(io, allocator, users_dir, &location.owner_id)) orelse return error.NotFound;│ │250 + defer owner_repo.deinit(io, allocator); │ │251 + const moment = try owner_repo.core.latestMoment(); │ │252 + const notif_id = idOf(&location.repo_id, thread_id); │ │253 + if (try isNotified(moment, &notif_id, user_id)) return; │ │254 + if (try tierFull(moment, &notif_id, .subscribed)) return; │ │255 + } │ │256 + try writeMode(io, allocator, users_dir, user_id, &location, thread_id, .subscribed, author); │ │257 } │ │258 │ │259 // never notify `user_id` of the thread `thread_id` in the repo at `location`, dropping its notif │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │266 thread_id: *const [evt.event_id_size]u8, │ │267 author: evt.CommitAuthor, │ │268 ) !void { │ │254 - try writeMode(io, allocator, users_dir, user_id, &location, thread_id, .mute, author); │ │269 + try writeMode(io, allocator, users_dir, user_id, &location, thread_id, .muted, author); │ │270 try dismiss(io, allocator, users_dir, user_id, &idOf(&location.repo_id, thread_id)); │ │256 - indexSubscriber(io, allocator, users_dir, location, thread_id, user_id, false); │ │271 } │ │272 │ │259 -// write the user's notify event for a thread │ │260 -fn writeMode( │ │273 +// write the user's notify event for a thread, then move them to its tier in the owner's index│ │274 +pub fn writeMode( │ │275 io: std.Io, │ │276 allocator: std.mem.Allocator, │ │277 users_dir: []const u8, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │281 mode: evt.Notify.Mode, │ │282 author: evt.CommitAuthor, │ │283 ) !void { │ │270 - var user_repo = (try evt.openUserRepo(io, allocator, users_dir, user_id)) orelse return error.NotFound; │ │271 - defer user_repo.deinit(io, allocator); │ │272 - try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &[_]evt.EventWithId{.{ │ │273 - .id = std.fmt.bytesToHex(evt.Notify.idOf(&location.repo_id, thread_id), .lower), │ │274 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │275 - .author = author, │ │276 - .event = .{ .notify = .{ │ │277 - .owner_id = std.fmt.bytesToHex(location.owner_id, .lower), │ │278 - .repo_id = std.fmt.bytesToHex(location.repo_id, .lower), │ │279 - .thread_id = std.fmt.bytesToHex(thread_id.*, .lower), │ │280 - .mode = mode, │ │281 - } }, │ │282 - }}); │ │284 + { │ │285 + var user_repo = (try evt.openUserRepo(io, allocator, users_dir, user_id)) orelse return error.NotFound; │ │286 + defer user_repo.deinit(io, allocator); │ │287 + try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &[_]evt.EventWithId{.{│ │288 + .id = std.fmt.bytesToHex(evt.Notify.idOf(&location.repo_id, thread_id), .lower), │ │289 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │290 + .author = author, │ │291 + .event = .{ .notify = .{ │ │292 + .owner_id = std.fmt.bytesToHex(location.owner_id, .lower), │ │293 + .repo_id = std.fmt.bytesToHex(location.repo_id, .lower), │ │294 + .thread_id = std.fmt.bytesToHex(thread_id.*, .lower), │ │295 + .mode = mode, │ │296 + } }, │ │297 + }}); │ │298 + } │ │299 + indexMode(io, allocator, users_dir, location, thread_id, user_id, mode); │ │300 } │ │301 │ │285 -// add `user_id` to the thread's subscribers in its repo owner's index, or remove │ │286 -// it, logging rather than returning a failure │ │287 -fn indexSubscriber( │ │302 +// move `user_id` to `mode`'s tier on the thread in its repo owner's index, or │ │303 +// out of every tier for a mute, logging rather than returning a failure │ │304 +fn indexMode( │ │305 io: std.Io, │ │306 allocator: std.mem.Allocator, │ │307 users_dir: []const u8, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────╮ │291 - location: evt.RepoLocation, │ │308 + location: *const evt.RepoLocation, │ │309 thread_id: *const [evt.event_id_size]u8,│ │310 user_id: *const [evt.event_id_size]u8, │ │294 - subscribed: bool, │ │311 + mode: evt.Notify.Mode, │ │312 ) void { │ │313 const Ctx = struct { │ │314 notif_id: [evt.event_id_size]u8, │ ╰─────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │315 user_id: *const [evt.event_id_size]u8, │ │299 - subscribed: bool, │ │316 + mode: evt.Notify.Mode, │ │317 │ │318 pub fn run(ctx: @This(), cursor: *UserDB.Cursor(.read_write)) !void {│ │319 const moment = try UserDB.HashMap(.read_write).init(cursor.*); │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │303 - const sets = try UserDB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(hash_kind, subscriber_map_key))); │ │304 - const subscribers = try UserDB.SortedSet(.read_write).init(try sets.putCursor(hash.hashInt(hash_kind, &ctx.notif_id))); │ │305 - if (ctx.subscribed) { │ │306 - try subscribers.put(ctx.user_id); │ │307 - } else if (!try subscribers.remove(ctx.user_id)) return error.CancelTransaction; │ │320 + inline for (comptime std.enums.values(evt.Notify.Mode)) |tier_mode| { │ │321 + if (comptime tierOf(tier_mode)) |tier| { │ │322 + const sets = try UserDB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(hash_kind, tier.map_key))); │ │323 + const users = try UserDB.SortedSet(.read_write).init(try sets.putCursor(hash.hashInt(hash_kind, &ctx.notif_id)));│ │324 + if (tier_mode == ctx.mode) try users.put(ctx.user_id) else _ = try users.remove(ctx.user_id); │ │325 + } │ │326 + } │ │327 } │ │328 }; │ │329 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯