This repository has no description
1//! the appview: a derived, disposable index over the repo.
2//!
3//! the repo (signed CAR) is the only source of truth. this layer exists so
4//! reads don't walk the MST, and it can be deleted and rebuilt from the repo
5//! at any time (`hydrate` does exactly that on boot).
6//!
7//! backends are swappable behind the union:
8//! - burner: embedded burner-redis, zero-infrastructure default
9//! - redis: a real redis over TCP — the production shape (one connection
10//! guarded by a mutex; pooling is the obvious next step if it shows up
11//! in profiles)
12
13const std = @import("std");
14const burner = @import("burner_redis");
15const redis = @import("redis");
16
17pub const Cache = union(enum) {
18 burner: Burner,
19 redis: Redis,
20
21 pub fn initBurner(allocator: std.mem.Allocator, io: std.Io) Cache {
22 return .{ .burner = .{ .store = burner.Store.init(allocator, io) } };
23 }
24
25 pub fn initRedis(allocator: std.mem.Allocator, io: std.Io, host: []const u8, port: u16) !Cache {
26 return .{ .redis = .{
27 .io = io,
28 .client = try redis.Client.connect(io, allocator, host, port),
29 } };
30 }
31
32 pub fn deinit(self: *Cache) void {
33 switch (self.*) {
34 .burner => |*b| b.store.deinit(),
35 .redis => |*r| r.client.close(),
36 }
37 }
38
39 pub fn set(self: *Cache, key: []const u8, value: []const u8) !void {
40 switch (self.*) {
41 .burner => |*b| _ = try b.store.set(key, value, .{}),
42 .redis => |*r| {
43 r.mutex.lock(r.io) catch return error.LockFailed;
44 defer r.mutex.unlock(r.io);
45 var cmds = r.client.strings();
46 try cmds.set(key, value);
47 },
48 }
49 }
50
51 /// returned slice is owned by out_alloc
52 pub fn get(self: *Cache, out_alloc: std.mem.Allocator, key: []const u8) ?[]u8 {
53 switch (self.*) {
54 .burner => |*b| return b.store.get(out_alloc, key) catch null,
55 .redis => |*r| {
56 r.mutex.lock(r.io) catch return null;
57 defer r.mutex.unlock(r.io);
58 var cmds = r.client.strings();
59 const val = cmds.get(key) catch return null;
60 const v = val orelse return null;
61 return out_alloc.dupe(u8, v) catch null;
62 },
63 }
64 }
65
66 // set ops back the appview's secondary indexes (runs by state, by flow)
67 // — the same shape as the cloud prototype's redis run-state indices.
68
69 pub fn sadd(self: *Cache, key: []const u8, member: []const u8) !void {
70 switch (self.*) {
71 .burner => |*b| _ = try b.store.sadd(key, &.{member}),
72 .redis => |*r| {
73 r.mutex.lock(r.io) catch return error.LockFailed;
74 defer r.mutex.unlock(r.io);
75 var cmds = r.client.sets();
76 _ = try cmds.sadd(key, &.{member});
77 },
78 }
79 }
80
81 pub fn srem(self: *Cache, key: []const u8, member: []const u8) !void {
82 switch (self.*) {
83 .burner => |*b| _ = b.store.srem(key, &.{member}) catch 0,
84 .redis => |*r| {
85 r.mutex.lock(r.io) catch return;
86 defer r.mutex.unlock(r.io);
87 var cmds = r.client.sets();
88 _ = cmds.srem(key, &.{member}) catch 0;
89 },
90 }
91 }
92
93 /// returned members owned by out_alloc
94 pub fn smembers(self: *Cache, out_alloc: std.mem.Allocator, key: []const u8) ![][]u8 {
95 switch (self.*) {
96 .burner => |*b| return b.store.smembers(out_alloc, key) catch &.{},
97 .redis => |*r| {
98 r.mutex.lock(r.io) catch return &.{};
99 defer r.mutex.unlock(r.io);
100 var cmds = r.client.sets();
101 const vals = cmds.smembers(key) catch return &.{};
102 var out = try out_alloc.alloc([]u8, vals.len);
103 var n: usize = 0;
104 for (vals) |v| {
105 switch (v) {
106 .bulk => |b| {
107 const bytes = b orelse continue;
108 out[n] = try out_alloc.dupe(u8, bytes);
109 n += 1;
110 },
111 else => {},
112 }
113 }
114 return out[0..n];
115 },
116 }
117 }
118
119 pub fn scard(self: *Cache, key: []const u8) i64 {
120 switch (self.*) {
121 .burner => |*b| return b.store.scard(key) catch 0,
122 .redis => |*r| {
123 r.mutex.lock(r.io) catch return 0;
124 defer r.mutex.unlock(r.io);
125 var cmds = r.client.sets();
126 return cmds.scard(key) catch 0;
127 },
128 }
129 }
130};
131
132const Burner = struct {
133 store: burner.Store,
134};
135
136const Redis = struct {
137 io: std.Io,
138 client: redis.Client,
139 mutex: std.Io.Mutex = .init,
140};
141
142test "cache set/get round trip" {
143 var threaded = std.Io.Threaded.init(std.testing.allocator, .{});
144 defer threaded.deinit();
145 const io = threaded.io();
146
147 var cache = Cache.initBurner(std.testing.allocator, io);
148 defer cache.deinit();
149
150 try cache.set("flow:name:hello", "abc-123");
151 const got = cache.get(std.testing.allocator, "flow:name:hello").?;
152 defer std.testing.allocator.free(got);
153 try std.testing.expectEqualStrings("abc-123", got);
154 try std.testing.expect(cache.get(std.testing.allocator, "missing") == null);
155}