╭ changed ──────────╮ │src/event/Issue.zig│ ╰───────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │ 5 │ │ 6 title: []const u8, │ │ 7 description: []const u8, │ │ 8 -tags: []const u8, │ │ 8 +tags: []const u8, // NUL-separated │ │ 9 created_ts: u64 = 0, // the commit timestamp of the event that first created this issue│ │10 + │ │11 +pub const tag_max_len = 64; │ │12 + │ │13 +pub fn tagIterator(tags: []const u8) std.mem.SplitIterator(u8, .scalar) { │ │14 + return std.mem.splitScalar(u8, tags, 0); │ │15 +} │ │16 │ │17 // `created_ts` comes from the commit timestamp, not the event │ │18 // payload, so it must not appear in the JSON we output │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │40 const event_id_to_issue_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "event-id->issue"));│ │41 const event_id_to_issue = try DB.HashMap(.read_write).init(event_id_to_issue_cursor); │ │42 │ │43 + // maps each tag to the set of issues carrying it, so a tag page can seek │ │44 + // its window the same way the unfiltered issue list does. │ │45 + const tag_to_issues_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "tag->issue-id-set")); │ │46 + const tag_to_issues = try DB.SortedMap(.read_write).init(tag_to_issues_cursor); │ │47 + │ │48 if (event_maybe) |event| { │ │49 var event_to_write = event; │ │50 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │54 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); │ │55 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue);│ │56 event_to_write.created_ts = existing_event.created_ts; │ │57 + │ │58 + // drop the old tags' entries; the current tags are re-added below │ │59 + const order_key = evt.orderKey(existing_event.created_ts, event_id); │ │60 + try removeFromTagSets(DB, tag_to_issues, existing_event.tags, &order_key); │ │61 } else { │ │62 // first time we've seen this issue: stamp it with the commit timestamp │ │63 event_to_write.created_ts = created_ts; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │66 const issue_cursor = try event_id_to_issue.putCursor(issue_key); │ │67 const issue = try DB.HashMap(.read_write).init(issue_cursor); │ │68 try evt.upsert(@This(), DB, hash_kind, issue, event_to_write); │ │69 + │ │70 + const order_key = evt.orderKey(event_to_write.created_ts, event_id); │ │71 + │ │72 + var tag_iter = tagIterator(event.tags); │ │73 + while (tag_iter.next()) |tag| { │ │74 + if (tag.len == 0) continue; │ │75 + if (tag.len > tag_max_len) return error.TagTooLong; │ │76 + const tag_set_cursor = try tag_to_issues.putCursor(tag); │ │77 + const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor);│ │78 + try tag_set.put(&order_key); │ │79 + } │ │80 │ │81 // first time we've seen this issue: add it to the set the issues view │ │82 // lists, ordered by creation time. the key (orderKey) embeds the event│ ╰───────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │84 if (existing_cursor_maybe == null) { │ │85 const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "issue-id-set"));│ │86 const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); │ │61 - const order_key = evt.orderKey(event_to_write.created_ts, event_id); │ │87 try issue_id_set.put(&order_key); │ │88 } │ │89 } else { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │65 - // drop it from the ordered set using its recorded creation timestamp │ │90 + // drop it from the ordered sets using its recorded creation timestamp │ │91 if (try event_id_to_issue.getCursor(issue_key)) |existing_cursor| { │ │92 const existing_issue = try DB.HashMap(.read_only).init(existing_cursor); │ │93 const existing_event = try evt.read(@This(), DB, hash_kind, arena, existing_issue);│ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 94 + const order_key = evt.orderKey(existing_event.created_ts, event_id); │ │ 95 + │ │ 96 const issue_id_set_cursor = try haxy_moment.putCursor(hash.hashInt(hash_kind, "issue-id-set"));│ │ 97 const issue_id_set = try DB.SortedSet(.read_write).init(issue_id_set_cursor); │ │ 71 - const order_key = evt.orderKey(existing_event.created_ts, event_id); │ │ 98 _ = try issue_id_set.remove(&order_key); │ │ 99 + │ │100 + try removeFromTagSets(DB, tag_to_issues, existing_event.tags, &order_key); │ │101 } │ │102 if (!try event_id_to_issue.remove(issue_key)) return error.EventNotFound; │ │103 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────╮ │104 } │ │105 │ │106 +// remove an issue's order key from each of its tags' sets, pruning tags whose│ │107 +// set becomes empty. │ │108 +fn removeFromTagSets( │ │109 + comptime DB: type, │ │110 + tag_to_issues: DB.SortedMap(.read_write), │ │111 + tags: []const u8, │ │112 + order_key: []const u8, │ │113 +) !void { │ │114 + var tag_iter = tagIterator(tags); │ │115 + while (tag_iter.next()) |tag| { │ │116 + if (tag.len == 0) continue; │ │117 + const tag_set_cursor = try tag_to_issues.putCursor(tag); │ │118 + const tag_set = try DB.SortedSet(.read_write).init(tag_set_cursor); │ │119 + _ = try tag_set.remove(order_key); │ │120 + if (0 == try tag_set.count()) _ = try tag_to_issues.remove(tag); │ │121 + } │ │122 +} │ │123 + │ ╰───────────────────────────────────────────────────────────────────────────────────╯