feat: component level caching
This commit is contained in:
parent
8ba9493706
commit
8aba678826
7 changed files with 117 additions and 100 deletions
|
|
@ -1,10 +1,18 @@
|
|||
var visit_count: usize = 0;
|
||||
pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
const allocator = ctx.arena;
|
||||
|
||||
if (visit_count == 3) {
|
||||
visit_count = 0;
|
||||
_ = zx.cache.delPrefix("cmp:time");
|
||||
} else {
|
||||
visit_count += 1;
|
||||
}
|
||||
|
||||
return (
|
||||
<div @allocator={allocator}>
|
||||
<h1>Time:</h1>
|
||||
<Time @caching="10s:time" />
|
||||
<h1>Time: {visit_count}</h1>
|
||||
<Time @caching="3s:time" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -21,7 +29,7 @@ fn Time(allocator: zx.Allocator) zx.Component {
|
|||
|
||||
pub const options = zx.PageOptions{
|
||||
.rendering = .server,
|
||||
// .caching = .tag("10s:time"),
|
||||
.caching = .{ .seconds = 1 },
|
||||
};
|
||||
|
||||
const zx = @import("zx");
|
||||
|
|
|
|||
|
|
@ -194,6 +194,18 @@ const PageCache = struct {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Delete a specific page from the cache by exact path
|
||||
/// Example: del("/users/123")
|
||||
pub fn del(self: *PageCache, path: []const u8) bool {
|
||||
return self.cache.del(path);
|
||||
}
|
||||
|
||||
/// Delete all pages matching a path prefix
|
||||
/// Example: delPath("/users") deletes /users, /users/1, /users/2, etc.
|
||||
pub fn delPath(self: *PageCache, path_prefix: []const u8) usize {
|
||||
return self.cache.delPrefix(path_prefix) catch 0;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Handler = struct {
|
||||
|
|
@ -202,10 +214,12 @@ pub const Handler = struct {
|
|||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, meta: *App.Meta, cache_config: CacheConfig) !Handler {
|
||||
// Initialize component-level cache
|
||||
_ = zx.ComponentCache.init(allocator);
|
||||
// Initialize unified component cache
|
||||
try zx.cache.init(allocator, .{
|
||||
.max_size = cache_config.max_size,
|
||||
});
|
||||
|
||||
return .{
|
||||
return Handler{
|
||||
.meta = meta,
|
||||
.allocator = allocator,
|
||||
.page_cache = try PageCache.init(allocator, cache_config),
|
||||
|
|
|
|||
166
src/root.zig
166
src/root.zig
|
|
@ -4,83 +4,68 @@
|
|||
const std = @import("std");
|
||||
pub const Ast = @import("zx/Ast.zig");
|
||||
pub const Parse = @import("zx/Parse.zig");
|
||||
const cachez = @import("cachez");
|
||||
|
||||
/// Client component rendering type - available on all targets
|
||||
/// Component-level cache for rendered HTML fragments
|
||||
pub const ComponentCache = struct {
|
||||
const Entry = struct {
|
||||
/// Global cache for components and pages
|
||||
pub const cache = struct {
|
||||
const CacheEntry = struct {
|
||||
html: []const u8,
|
||||
expires_at: i64,
|
||||
|
||||
pub fn removedFromCache(self: CacheEntry, allocator: std.mem.Allocator) void {
|
||||
allocator.free(self.html);
|
||||
}
|
||||
};
|
||||
|
||||
entries: std.StringHashMap(Entry),
|
||||
mutex: std.Thread.Mutex,
|
||||
allocator: std.mem.Allocator,
|
||||
var inner: ?cachez.Cache(CacheEntry) = null;
|
||||
var alloc: ?std.mem.Allocator = null;
|
||||
|
||||
var instance: ?*ComponentCache = null;
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) *ComponentCache {
|
||||
if (instance) |i| return i;
|
||||
|
||||
const cache = allocator.create(ComponentCache) catch @panic("OOM");
|
||||
cache.* = .{
|
||||
.entries = std.StringHashMap(Entry).init(allocator),
|
||||
.mutex = .{},
|
||||
.allocator = allocator,
|
||||
};
|
||||
instance = cache;
|
||||
return cache;
|
||||
/// Initialize the cache (called once at app startup)
|
||||
pub fn init(allocator: std.mem.Allocator, config: cachez.Config) !void {
|
||||
if (inner != null) return;
|
||||
alloc = allocator;
|
||||
inner = try cachez.Cache(CacheEntry).init(allocator, config);
|
||||
}
|
||||
|
||||
pub fn global() ?*ComponentCache {
|
||||
return instance;
|
||||
/// Deinitialize the cache
|
||||
pub fn deinit() void {
|
||||
if (inner) |*c| {
|
||||
c.deinit();
|
||||
inner = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: *ComponentCache, key: []const u8) ?[]const u8 {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
if (self.entries.get(key)) |entry| {
|
||||
const now = std.time.timestamp();
|
||||
if (now < entry.expires_at) {
|
||||
return entry.html;
|
||||
}
|
||||
// Expired - remove it
|
||||
if (self.entries.fetchRemove(key)) |kv| {
|
||||
self.allocator.free(kv.key);
|
||||
self.allocator.free(kv.value.html);
|
||||
/// Get cached HTML by key
|
||||
pub fn get(key: []const u8) ?[]const u8 {
|
||||
if (inner) |*c| {
|
||||
if (c.get(key)) |entry| {
|
||||
defer entry.release();
|
||||
return entry.value.html;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn put(self: *ComponentCache, key: []const u8, html: []const u8, ttl_seconds: u32) void {
|
||||
self.mutex.lock();
|
||||
defer self.mutex.unlock();
|
||||
|
||||
const now = std.time.timestamp();
|
||||
const expires_at = now + @as(i64, @intCast(ttl_seconds));
|
||||
|
||||
// Copy key and html
|
||||
const key_copy = self.allocator.dupe(u8, key) catch return;
|
||||
const html_copy = self.allocator.dupe(u8, html) catch {
|
||||
self.allocator.free(key_copy);
|
||||
return;
|
||||
};
|
||||
|
||||
// Remove old entry if exists
|
||||
if (self.entries.fetchRemove(key)) |old| {
|
||||
self.allocator.free(old.key);
|
||||
self.allocator.free(old.value.html);
|
||||
/// Store HTML in cache with TTL
|
||||
pub fn put(key: []const u8, html: []const u8, ttl_seconds: u32) void {
|
||||
if (inner) |*c| {
|
||||
const allocator = alloc orelse return;
|
||||
const html_copy = allocator.dupe(u8, html) catch return;
|
||||
c.put(key, .{ .html = html_copy }, .{ .ttl = ttl_seconds }) catch {
|
||||
allocator.free(html_copy);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
self.entries.put(key_copy, .{
|
||||
.html = html_copy,
|
||||
.expires_at = expires_at,
|
||||
}) catch {
|
||||
self.allocator.free(key_copy);
|
||||
self.allocator.free(html_copy);
|
||||
};
|
||||
/// Delete cache entry by exact key
|
||||
pub fn del(key: []const u8) bool {
|
||||
if (inner) |*c| return c.del(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Delete all cache entries matching a prefix
|
||||
pub fn delPrefix(prefix: []const u8) usize {
|
||||
if (inner) |*c| return c.delPrefix(prefix) catch 0;
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -472,7 +457,6 @@ pub const Component = union(enum) {
|
|||
async_mode: BuiltinAttribute.Async = .sync,
|
||||
fallback: ?*const Component = null,
|
||||
caching: ?BuiltinAttribute.Caching = null,
|
||||
cache_key: ?[]const u8 = null,
|
||||
|
||||
pub fn init(comptime func: anytype, allocator: Allocator, props: anytype) ComponentFn {
|
||||
const FuncInfo = @typeInfo(@TypeOf(func));
|
||||
|
|
@ -693,41 +677,41 @@ pub const Component = union(enum) {
|
|||
// Check for component-level caching
|
||||
if (func.caching) |caching| {
|
||||
if (caching.seconds > 0) {
|
||||
// Generate cache key from function pointer + props pointer
|
||||
var key_buf: [64]u8 = undefined;
|
||||
const cache_key = if (func.cache_key) |ck|
|
||||
ck
|
||||
else blk: {
|
||||
const key_len = std.fmt.bufPrint(&key_buf, "cmp:{x}:{x}", .{
|
||||
// Generate cache key from function pointer + props pointer + optional custom key
|
||||
var key_buf: [128]u8 = undefined;
|
||||
const generated_key = if (caching.key) |custom_key|
|
||||
std.fmt.bufPrint(&key_buf, "cmp:{s}:{x}:{x}", .{
|
||||
custom_key,
|
||||
@intFromPtr(func.callFn),
|
||||
@intFromPtr(func.propsPtr),
|
||||
}) catch break :blk null;
|
||||
break :blk key_buf[0..key_len.len];
|
||||
};
|
||||
}) catch null
|
||||
else
|
||||
std.fmt.bufPrint(&key_buf, "cmp:{x}:{x}", .{
|
||||
@intFromPtr(func.callFn),
|
||||
@intFromPtr(func.propsPtr),
|
||||
}) catch null;
|
||||
|
||||
if (cache_key) |key| {
|
||||
if (generated_key) |key| {
|
||||
// Try to get from cache
|
||||
if (ComponentCache.global()) |cache| {
|
||||
if (cache.get(key)) |cached_html| {
|
||||
try writer.writeAll(cached_html);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render to buffer for caching
|
||||
var buf_writer = std.io.Writer.Allocating.init(func.allocator);
|
||||
const component = func.call() catch |err| {
|
||||
std.debug.print("Error rendering component: {}\n", .{err});
|
||||
return err;
|
||||
};
|
||||
try component.renderInner(&buf_writer.writer, options);
|
||||
|
||||
const rendered = buf_writer.written();
|
||||
cache.put(key, rendered, caching.seconds);
|
||||
|
||||
// Write to actual output
|
||||
try writer.writeAll(rendered);
|
||||
if (cache.get(key)) |cached_html| {
|
||||
try writer.writeAll(cached_html);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render to buffer for caching
|
||||
var buf_writer = std.io.Writer.Allocating.init(func.allocator);
|
||||
const component = func.call() catch |err| {
|
||||
std.debug.print("Error rendering component: {}\n", .{err});
|
||||
return err;
|
||||
};
|
||||
try component.renderInner(&buf_writer.writer, options);
|
||||
|
||||
const rendered = buf_writer.written();
|
||||
cache.put(key, rendered, caching.seconds);
|
||||
|
||||
// Write to actual output
|
||||
try writer.writeAll(rendered);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
test/data/component/caching.html
generated
2
test/data/component/caching.html
generated
|
|
@ -1 +1 @@
|
|||
<main><button>Custom Button</button></main>
|
||||
<main><button>Custom Button</button><button>Custom Button</button></main>
|
||||
|
|
@ -10,6 +10,11 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
|||
.{ .caching = comptime .tag("10s:button") },
|
||||
.{ .title = "Custom Button" },
|
||||
),
|
||||
_zx.cmp(
|
||||
Button,
|
||||
.{ .caching = comptime .tag("10s") },
|
||||
.{ .title = "Custom Button" },
|
||||
),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ pub fn Page(allocator: zx.Allocator) zx.Component {
|
|||
return (
|
||||
<main @allocator={allocator}>
|
||||
<Button @caching="10s:button" title="Custom Button" />
|
||||
// <Button @caching={.{ .seconds = 10, .key = "button" }} title="Custom Button" />
|
||||
// <Button @caching={.{ .seconds = 10 }} title="Custom Button" />
|
||||
<Button @caching="10s" title="Custom Button" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
7
vendor/cachez/src/segment.zig
vendored
7
vendor/cachez/src/segment.zig
vendored
|
|
@ -265,8 +265,11 @@ pub fn Segment(comptime T: type) type {
|
|||
var lookup = &self.lookup;
|
||||
self.mutex.lock();
|
||||
for (entries) |entry| {
|
||||
self.size -= entry._size;
|
||||
_ = lookup.remove(entry.key);
|
||||
// Use saturating subtraction to handle race condition where
|
||||
// entry was already deleted between shared and exclusive lock
|
||||
if (lookup.remove(entry.key)) {
|
||||
self.size = self.size -| entry._size;
|
||||
}
|
||||
}
|
||||
self.mutex.unlock();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue