this repo has no description coral.waow.tech
at main 222 lines 7.7 kB view raw
1const std = @import("std"); 2const Mutex = std.Thread.Mutex; 3const Atomic = std.atomic.Value; 4 5// top entities tracking (like TopPosters pattern) 6pub const MAX_ENTITIES = 100; 7const TOP_N = 10; 8 9pub const EntityEntry = struct { 10 text: [64]u8 = undefined, 11 text_len: u8 = 0, 12 label: [16]u8 = undefined, 13 label_len: u8 = 0, 14 count: u32 = 0, 15 16 pub fn getText(self: *const EntityEntry) []const u8 { 17 return self.text[0..self.text_len]; 18 } 19 20 pub fn getLabel(self: *const EntityEntry) []const u8 { 21 return self.label[0..self.label_len]; 22 } 23 24 fn setText(self: *EntityEntry, text: []const u8) void { 25 const len: u8 = @intCast(@min(text.len, 64)); 26 @memcpy(self.text[0..len], text[0..len]); 27 self.text_len = len; 28 } 29 30 fn setLabel(self: *EntityEntry, label: []const u8) void { 31 const len: u8 = @intCast(@min(label.len, 16)); 32 @memcpy(self.label[0..len], label[0..len]); 33 self.label_len = len; 34 } 35}; 36 37pub const TopEntities = struct { 38 entries: [MAX_ENTITIES]EntityEntry = [_]EntityEntry{.{}} ** MAX_ENTITIES, 39 count: usize = 0, 40 mutex: Mutex = .{}, 41 42 /// record an entity occurrence (case-insensitive) 43 pub fn record(self: *TopEntities, text: []const u8, label: []const u8) void { 44 if (text.len == 0) return; 45 46 self.mutex.lock(); 47 defer self.mutex.unlock(); 48 49 // normalize: lowercase for comparison 50 var normalized: [64]u8 = undefined; 51 const norm_len = @min(text.len, 64); 52 for (0..norm_len) |i| { 53 normalized[i] = std.ascii.toLower(text[i]); 54 } 55 const norm_text = normalized[0..norm_len]; 56 57 // look for existing entry (case-insensitive) 58 for (self.entries[0..self.count]) |*entry| { 59 var entry_norm: [64]u8 = undefined; 60 for (0..entry.text_len) |i| { 61 entry_norm[i] = std.ascii.toLower(entry.text[i]); 62 } 63 if (std.mem.eql(u8, entry_norm[0..entry.text_len], norm_text)) { 64 entry.count += 1; 65 return; 66 } 67 } 68 69 // add new entry if space 70 if (self.count < MAX_ENTITIES) { 71 self.entries[self.count].setText(text); 72 self.entries[self.count].setLabel(label); 73 self.entries[self.count].count = 1; 74 self.count += 1; 75 } 76 } 77 78 /// get top N entities sorted by count 79 pub fn getTop(self: *TopEntities, comptime N: usize) [N]EntityEntry { 80 self.mutex.lock(); 81 defer self.mutex.unlock(); 82 83 // copy and sort 84 var sorted: [MAX_ENTITIES]EntityEntry = self.entries; 85 const slice = sorted[0..self.count]; 86 87 std.mem.sort(EntityEntry, slice, {}, struct { 88 fn cmp(_: void, a: EntityEntry, b: EntityEntry) bool { 89 return a.count > b.count; 90 } 91 }.cmp); 92 93 var result: [N]EntityEntry = [_]EntityEntry{.{}} ** N; 94 const copy_len = @min(N, self.count); 95 @memcpy(result[0..copy_len], slice[0..copy_len]); 96 return result; 97 } 98 99 /// serialize top N to JSON using std.json.Stringify 100 pub fn toJson(self: *TopEntities, comptime N: usize, buf: []u8) []const u8 { 101 const top_entries = self.getTop(N); 102 103 var w: std.Io.Writer = .fixed(buf); 104 var jw: std.json.Stringify = .{ .writer = &w }; 105 106 jw.beginArray() catch return "[]"; 107 for (top_entries) |entry| { 108 if (entry.count == 0) continue; 109 jw.beginObject() catch break; 110 jw.objectField("text") catch break; 111 jw.write(entry.getText()) catch break; 112 jw.objectField("label") catch break; 113 jw.write(entry.getLabel()) catch break; 114 jw.objectField("count") catch break; 115 jw.write(entry.count) catch break; 116 jw.endObject() catch break; 117 } 118 jw.endArray() catch return "[]"; 119 120 return w.buffered(); 121 } 122}; 123 124// recent entities ring buffer (for live feed) 125pub const RECENT_SIZE = 32; 126 127pub const RecentEntity = struct { 128 text: [64]u8 = undefined, 129 text_len: u8 = 0, 130 label: [16]u8 = undefined, 131 label_len: u8 = 0, 132 133 pub fn getText(self: *const RecentEntity) []const u8 { 134 return self.text[0..self.text_len]; 135 } 136 137 pub fn getLabel(self: *const RecentEntity) []const u8 { 138 return self.label[0..self.label_len]; 139 } 140}; 141 142pub const RecentRing = struct { 143 entries: [RECENT_SIZE]RecentEntity = [_]RecentEntity{.{}} ** RECENT_SIZE, 144 head: Atomic(usize) = Atomic(usize).init(0), // next write position 145 version: Atomic(u64) = Atomic(u64).init(0), // increments on write 146 147 /// push a new entity (overwrites oldest) 148 pub fn push(self: *RecentRing, text: []const u8, label: []const u8) void { 149 if (text.len == 0) return; 150 151 const pos = self.head.load(.monotonic); 152 const entry = &self.entries[pos]; 153 154 const text_len: u8 = @intCast(@min(text.len, 64)); 155 @memcpy(entry.text[0..text_len], text[0..text_len]); 156 entry.text_len = text_len; 157 158 const label_len: u8 = @intCast(@min(label.len, 16)); 159 @memcpy(entry.label[0..label_len], label[0..label_len]); 160 entry.label_len = label_len; 161 162 // advance head 163 self.head.store((pos + 1) % RECENT_SIZE, .monotonic); 164 _ = self.version.fetchAdd(1, .monotonic); 165 } 166 167 /// get recent entries since last_version, returns (entries, new_version) 168 pub fn getSince(self: *RecentRing, last_version: u64) struct { entries: []const RecentEntity, version: u64 } { 169 const current_version = self.version.load(.monotonic); 170 if (current_version == last_version) { 171 return .{ .entries = &[_]RecentEntity{}, .version = current_version }; 172 } 173 174 // how many new entries? 175 const delta = current_version - last_version; 176 const count = @min(delta, RECENT_SIZE); 177 178 // read backwards from head 179 const head = self.head.load(.monotonic); 180 var start: usize = undefined; 181 if (head >= count) { 182 start = head - count; 183 } else { 184 start = RECENT_SIZE - (count - head); 185 } 186 187 // return slice (might wrap, but we'll just return from start to head) 188 // for simplicity, return contiguous slice even if it means fewer entries 189 if (start < head) { 190 return .{ .entries = self.entries[start..head], .version = current_version }; 191 } else { 192 // wrapped - just return the newer portion 193 return .{ .entries = self.entries[0..head], .version = current_version }; 194 } 195 } 196 197 /// serialize recent entries to JSON array using std.json 198 pub fn toJson(self: *RecentRing, last_version: u64, buf: []u8) struct { json: []const u8, version: u64 } { 199 const result = self.getSince(last_version); 200 201 var w: std.Io.Writer = .fixed(buf); 202 var jw: std.json.Stringify = .{ .writer = &w }; 203 204 jw.beginArray() catch return .{ .json = "[]", .version = result.version }; 205 for (result.entries) |entry| { 206 if (entry.text_len == 0) continue; 207 jw.beginObject() catch break; 208 jw.objectField("text") catch break; 209 jw.write(entry.getText()) catch break; 210 jw.objectField("label") catch break; 211 jw.write(entry.getLabel()) catch break; 212 jw.endObject() catch break; 213 } 214 jw.endArray() catch return .{ .json = "[]", .version = result.version }; 215 216 return .{ .json = w.buffered(), .version = result.version }; 217 } 218}; 219 220// global instances 221pub var top: TopEntities = .{}; 222pub var recent: RecentRing = .{};