this repo has no description
coral.waow.tech
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 = .{};