const std = @import("std"); pub fn DocumentCache(comptime capacity: usize) type { if (capacity == 0) @compileError("DocumentCache requires at least one entry"); return struct { const Self = @This(); const Entry = struct { url: []u8, body: []u8, expires_at: i64, }; allocator: std.mem.Allocator, mutex: std.atomic.Mutex = .unlocked, entries: [capacity]?Entry = .{null} ** capacity, next: usize = 0, pub fn init(allocator: std.mem.Allocator) Self { return .{ .allocator = allocator }; } pub fn deinit(self: *Self) void { self.lock(); defer self.mutex.unlock(); for (&self.entries) |*slot| self.freeEntry(slot); } pub fn get(self: *Self, allocator: std.mem.Allocator, url: []const u8, now: i64) !?[]u8 { self.lock(); defer self.mutex.unlock(); for (&self.entries) |*slot| { const entry = slot.* orelse continue; if (!std.mem.eql(u8, entry.url, url)) continue; if (entry.expires_at <= now) { self.freeEntry(slot); return null; } return try allocator.dupe(u8, entry.body); } return null; } pub fn put(self: *Self, url: []const u8, body: []const u8, expires_at: i64) !void { const owned_url = try self.allocator.dupe(u8, url); errdefer self.allocator.free(owned_url); const owned_body = try self.allocator.dupe(u8, body); errdefer self.allocator.free(owned_body); self.lock(); defer self.mutex.unlock(); var index: ?usize = null; for (&self.entries, 0..) |*slot, i| { if (slot.* == null) { if (index == null) index = i; continue; } if (std.mem.eql(u8, slot.*.?.url, url)) { index = i; break; } } const selected = index orelse self.next; self.freeEntry(&self.entries[selected]); self.entries[selected] = .{ .url = owned_url, .body = owned_body, .expires_at = expires_at, }; self.next = (selected + 1) % capacity; } fn lock(self: *Self) void { while (!self.mutex.tryLock()) std.Thread.yield() catch {}; } fn freeEntry(self: *Self, slot: *?Entry) void { if (slot.*) |entry| { self.allocator.free(entry.url); self.allocator.free(entry.body); slot.* = null; } } }; } test "document cache returns fresh values and expires stale values" { var cache = DocumentCache(2).init(std.testing.allocator); defer cache.deinit(); try cache.put("https://client.example/metadata", "{\"name\":\"client\"}", 20); const fresh = (try cache.get(std.testing.allocator, "https://client.example/metadata", 19)).?; defer std.testing.allocator.free(fresh); try std.testing.expectEqualStrings("{\"name\":\"client\"}", fresh); try std.testing.expect((try cache.get(std.testing.allocator, "https://client.example/metadata", 20)) == null); } test "document cache replaces matching entries and evicts at capacity" { var cache = DocumentCache(2).init(std.testing.allocator); defer cache.deinit(); try cache.put("https://client.example/a", "old", 20); try cache.put("https://client.example/a", "new", 30); const replaced = (try cache.get(std.testing.allocator, "https://client.example/a", 10)).?; defer std.testing.allocator.free(replaced); try std.testing.expectEqualStrings("new", replaced); try cache.put("https://client.example/b", "b", 30); try cache.put("https://client.example/c", "c", 30); try std.testing.expect((try cache.get(std.testing.allocator, "https://client.example/a", 10)) == null); }