feat: add TtlAction and fromByte to protocol

This commit is contained in:
dualfroz committed 2026-09-07 18:46:10 +02:00
1 parent 4c072af95d
commit fc90b6c8a3
1 file changed
+176
+176
View File
@@ -313,3 +313,179 @@ pub fn decodeUserList(allocator: Allocator, payload: []const u8) ![]authmod.User
// --- ttl_op --- // --- ttl_op ---
/// Remote counterpart of the CLI's local `expire`/`persist`/`ttl`/
/// `expire-sweep` (main.zig, backed by `ttl.zig`), carried as one opcode the
/// same way `user_admin` bundles five user-management actions under one
/// opcode - see server.zig's `requiredRole`, which (unlike `user_admin`'s
/// blanket `admin` gate) inspects this action byte: `query` is read-only
/// and needs only `readonly`; `expire`/`persist`/`sweep` all mutate (sweep
/// deletes every due key) and need `readwrite`, matching every other write
/// opcode.
pub const TtlAction = enum(u8) {
expire = 0,
persist = 1,
query = 2,
sweep = 3,
pub fn fromByte(b: u8) !TtlAction {
return std.meta.intToEnum(TtlAction, b) catch error.Malformed;
}
};
/// `seconds` is only meaningful for `expire` - callers send 0 for the other
/// three actions, same "unused fields carry a zero" convention as
/// `encodeUserAdmin`'s `role`/`rounds`. `key` is empty for `sweep`, which
/// operates on every due key in the store rather than one specific key.
pub fn encodeTtlOp(allocator: Allocator, action: TtlAction, key: []const u8, seconds: u64) ![]u8 {
var buf = std.ArrayList(u8).init(allocator);
errdefer buf.deinit();
const w = encode.ByteWriter.init(&buf);
try w.byte(@intFromEnum(action));
try w.putU64(seconds);
try w.slice(key);
return buf.toOwnedSlice();
}
pub const TtlOpArgs = struct { action: TtlAction, seconds: u64, key: []const u8 };
pub fn decodeTtlOp(payload: []const u8) !TtlOpArgs {
var c = Cursor{ .data = payload };
const action = try TtlAction.fromByte(try c.takeByte());
const seconds = try c.takeU64();
const key = c.data[c.pos..];
return .{ .action = action, .seconds = seconds, .key = key };
}
/// Encodes the response to a `query` ttl_op: reuses `ttlmod.TtlResult`
/// rather than inventing a parallel type, the same way `encodeStats` reuses
/// `dbmod.Stats` and `encodeUserList` reuses `authmod.UserInfo`.
pub fn encodeTtlResult(allocator: Allocator, result: ttlmod.TtlResult) ![]u8 {
var buf = std.ArrayList(u8).init(allocator);
errdefer buf.deinit();
const w = encode.ByteWriter.init(&buf);
switch (result) {
.noKey => try w.byte(0),
.noExpiry => try w.byte(1),
.seconds => |s| {
try w.byte(2);
try w.putU64(s);
},
}
return buf.toOwnedSlice();
}
pub fn decodeTtlResult(payload: []const u8) !ttlmod.TtlResult {
var c = Cursor{ .data = payload };
const kind = try c.takeByte();
return switch (kind) {
0 => .noKey,
1 => .noExpiry,
2 => .{ .seconds = try c.takeU64() },
else => error.Malformed,
};
}
/// Encodes the response to a `sweep` ttl_op: how many due keys were reaped.
pub fn encodeTtlSweepCount(allocator: Allocator, count: u64) ![]u8 {
var buf = std.ArrayList(u8).init(allocator);
errdefer buf.deinit();
const w = encode.ByteWriter.init(&buf);
try w.putU64(count);
return buf.toOwnedSlice();
}
pub fn decodeTtlSweepCount(payload: []const u8) !u64 {
var c = Cursor{ .data = payload };
return c.takeU64();
}
// --- coll_ttl_op ---
/// Remote counterpart of the CLI's local `coll <kind> expire|ttl|persist
/// <name>`/`coll expire-sweep` (main.zig, backed by `collttl.zig`), same
/// bundling shape as `ttl_op` above but for a whole typed collection
/// (Counter/Hash/Set/List/ZSet) instead of one flat key - so the payload
/// carries an extra `Kind` tag byte the flat version does not need.
/// Reuses `TtlAction` directly rather than a parallel enum: the four
/// actions (expire/persist/query/sweep) mean exactly the same thing here,
/// just against `collttl.zig` instead of `ttl.zig`.
pub fn encodeCollTtlOp(allocator: Allocator, action: TtlAction, kind: collmod.Kind, name: []const u8, seconds: u64) ![]u8 {
var buf = std.ArrayList(u8).init(allocator);
errdefer buf.deinit();
const w = encode.ByteWriter.init(&buf);
try w.byte(@intFromEnum(action));
// `sweep` operates across every kind at once (collttl.sweepExpired
// scans the whole marker namespace, not one kind's slice of it), so
// the kind byte is meaningless for that action - callers pass any
// valid `Kind` and `decodeCollTtlOp` still decodes it, but
// `handleCollTtlOp` never reads it for `.sweep`. Same "unused field
// still round-trips" convention as `encodeTtlOp`'s zeroed `seconds`
// on non-`expire` actions.
try w.byte(kind.tagByte());
try w.putU64(seconds);
try w.slice(name);
return buf.toOwnedSlice();
}
pub const CollTtlOpArgs = struct { action: TtlAction, kind: collmod.Kind, seconds: u64, name: []const u8 };
pub fn decodeCollTtlOp(payload: []const u8) !CollTtlOpArgs {
var c = Cursor{ .data = payload };
const action = try TtlAction.fromByte(try c.takeByte());
const kind = collmod.Kind.fromTagByte(try c.takeByte()) orelse return error.Malformed;
const seconds = try c.takeU64();
const name = c.data[c.pos..];
return .{ .action = action, .kind = kind, .seconds = seconds, .name = name };
}
// --- coll_op ---
/// Remote counterpart of the CLI's local `coll <kind> <op> ...` (main.zig's
/// `runColl`, backed by `collections.zig`) - until now the Redis-like
/// collections (Counter/Hash/Set/List/ZSet) were reachable only from a
/// process that had the `Db` open directly, the same gap `ttl_op`/
/// `coll_ttl_op` already closed for TTL management. One opcode bundles
/// every collection operation (the same "one opcode, many actions" shape
/// as `user_admin` and `ttl_op`) rather than one opcode per op, since the
/// five kinds share almost nothing about their argument count or type.
/// The payload carries only the action tag, the collection name, and a
/// variable list of string arguments in the exact CLI text form `runColl`
/// already parses locally (values/members/indices/scores) - so
/// `handleCollOp` (server.zig) can reuse the very same
/// `std.fmt.parseInt`/`parseFloat` calls instead of inventing a second
/// binary number encoding just for this opcode.
pub const CollAction = enum(u8) {
counter_get = 0,
counter_set = 1,
counter_incrby = 2,
hash_set = 3,
hash_get = 4,
hash_del = 5,
hash_len = 6,
hash_getall = 7,
set_add = 8,
set_rem = 9,
set_ismember = 10,
set_card = 11,
set_members = 12,
// `set_inter`/`set_union`/`set_diff`: `name` is the first set, `args`
// are every remaining set name - the same split `runColl` already uses
// (`args[0]` vs `args[1..]`) for these three multi-set operations.
set_inter = 13,
set_union = 14,
set_diff = 15,
list_lpush = 16,
list_rpush = 17,
list_lpop = 18,
list_rpop = 19,
list_len = 20,
list_range = 21,
zset_add = 22,
zset_score = 23,
zset_incrby = 24,
zset_rem = 25,
zset_card = 26,
zset_rangebyrank = 27,
zset_rangebyscore = 28,
zset_rank = 29,