const std = @import("std"); const Mutex = std.Thread.Mutex; const Atomic = std.atomic.Value; // top entities tracking (like TopPosters pattern) pub const MAX_ENTITIES = 100; const TOP_N = 10; pub const EntityEntry = struct { text: [64]u8 = undefined, text_len: u8 = 0, label: [16]u8 = undefined, label_len: u8 = 0, count: u32 = 0, pub fn getText(self: *const EntityEntry) []const u8 { return self.text[0..self.text_len]; } pub fn getLabel(self: *const EntityEntry) []const u8 { return self.label[0..self.label_len]; } fn setText(self: *EntityEntry, text: []const u8) void { const len: u8 = @intCast(@min(text.len, 64)); @memcpy(self.text[0..len], text[0..len]); self.text_len = len; } fn setLabel(self: *EntityEntry, label: []const u8) void { const len: u8 = @intCast(@min(label.len, 16)); @memcpy(self.label[0..len], label[0..len]); self.label_len = len; } }; pub const TopEntities = struct { entries: [MAX_ENTITIES]EntityEntry = [_]EntityEntry{.{}} ** MAX_ENTITIES, count: usize = 0, mutex: Mutex = .{}, /// record an entity occurrence (case-insensitive) pub fn record(self: *TopEntities, text: []const u8, label: []const u8) void { if (text.len == 0) return; self.mutex.lock(); defer self.mutex.unlock(); // normalize: lowercase for comparison var normalized: [64]u8 = undefined; const norm_len = @min(text.len, 64); for (0..norm_len) |i| { normalized[i] = std.ascii.toLower(text[i]); } const norm_text = normalized[0..norm_len]; // look for existing entry (case-insensitive) for (self.entries[0..self.count]) |*entry| { var entry_norm: [64]u8 = undefined; for (0..entry.text_len) |i| { entry_norm[i] = std.ascii.toLower(entry.text[i]); } if (std.mem.eql(u8, entry_norm[0..entry.text_len], norm_text)) { entry.count += 1; return; } } // add new entry if space if (self.count < MAX_ENTITIES) { self.entries[self.count].setText(text); self.entries[self.count].setLabel(label); self.entries[self.count].count = 1; self.count += 1; } } /// get top N entities sorted by count pub fn getTop(self: *TopEntities, comptime N: usize) [N]EntityEntry { self.mutex.lock(); defer self.mutex.unlock(); // copy and sort var sorted: [MAX_ENTITIES]EntityEntry = self.entries; const slice = sorted[0..self.count]; std.mem.sort(EntityEntry, slice, {}, struct { fn cmp(_: void, a: EntityEntry, b: EntityEntry) bool { return a.count > b.count; } }.cmp); var result: [N]EntityEntry = [_]EntityEntry{.{}} ** N; const copy_len = @min(N, self.count); @memcpy(result[0..copy_len], slice[0..copy_len]); return result; } /// serialize top N to JSON using std.json.Stringify pub fn toJson(self: *TopEntities, comptime N: usize, buf: []u8) []const u8 { const top_entries = self.getTop(N); var w: std.Io.Writer = .fixed(buf); var jw: std.json.Stringify = .{ .writer = &w }; jw.beginArray() catch return "[]"; for (top_entries) |entry| { if (entry.count == 0) continue; jw.beginObject() catch break; jw.objectField("text") catch break; jw.write(entry.getText()) catch break; jw.objectField("label") catch break; jw.write(entry.getLabel()) catch break; jw.objectField("count") catch break; jw.write(entry.count) catch break; jw.endObject() catch break; } jw.endArray() catch return "[]"; return w.buffered(); } }; // recent entities ring buffer (for live feed) pub const RECENT_SIZE = 32; pub const RecentEntity = struct { text: [64]u8 = undefined, text_len: u8 = 0, label: [16]u8 = undefined, label_len: u8 = 0, pub fn getText(self: *const RecentEntity) []const u8 { return self.text[0..self.text_len]; } pub fn getLabel(self: *const RecentEntity) []const u8 { return self.label[0..self.label_len]; } }; pub const RecentRing = struct { entries: [RECENT_SIZE]RecentEntity = [_]RecentEntity{.{}} ** RECENT_SIZE, head: Atomic(usize) = Atomic(usize).init(0), // next write position version: Atomic(u64) = Atomic(u64).init(0), // increments on write /// push a new entity (overwrites oldest) pub fn push(self: *RecentRing, text: []const u8, label: []const u8) void { if (text.len == 0) return; const pos = self.head.load(.monotonic); const entry = &self.entries[pos]; const text_len: u8 = @intCast(@min(text.len, 64)); @memcpy(entry.text[0..text_len], text[0..text_len]); entry.text_len = text_len; const label_len: u8 = @intCast(@min(label.len, 16)); @memcpy(entry.label[0..label_len], label[0..label_len]); entry.label_len = label_len; // advance head self.head.store((pos + 1) % RECENT_SIZE, .monotonic); _ = self.version.fetchAdd(1, .monotonic); } /// get recent entries since last_version, returns (entries, new_version) pub fn getSince(self: *RecentRing, last_version: u64) struct { entries: []const RecentEntity, version: u64 } { const current_version = self.version.load(.monotonic); if (current_version == last_version) { return .{ .entries = &[_]RecentEntity{}, .version = current_version }; } // how many new entries? const delta = current_version - last_version; const count = @min(delta, RECENT_SIZE); // read backwards from head const head = self.head.load(.monotonic); var start: usize = undefined; if (head >= count) { start = head - count; } else { start = RECENT_SIZE - (count - head); } // return slice (might wrap, but we'll just return from start to head) // for simplicity, return contiguous slice even if it means fewer entries if (start < head) { return .{ .entries = self.entries[start..head], .version = current_version }; } else { // wrapped - just return the newer portion return .{ .entries = self.entries[0..head], .version = current_version }; } } /// serialize recent entries to JSON array using std.json pub fn toJson(self: *RecentRing, last_version: u64, buf: []u8) struct { json: []const u8, version: u64 } { const result = self.getSince(last_version); var w: std.Io.Writer = .fixed(buf); var jw: std.json.Stringify = .{ .writer = &w }; jw.beginArray() catch return .{ .json = "[]", .version = result.version }; for (result.entries) |entry| { if (entry.text_len == 0) continue; jw.beginObject() catch break; jw.objectField("text") catch break; jw.write(entry.getText()) catch break; jw.objectField("label") catch break; jw.write(entry.getLabel()) catch break; jw.endObject() catch break; } jw.endArray() catch return .{ .json = "[]", .version = result.version }; return .{ .json = w.buffered(), .version = result.version }; } }; // global instances pub var top: TopEntities = .{}; pub var recent: RecentRing = .{};