From fc90b6c8a3646a6d6151b7268f7b28b46b2f4c08 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 18:46:10 +0200 Subject: [PATCH] feat: add TtlAction and fromByte to protocol --- src/protocol.zig | 176 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) diff --git a/src/protocol.zig b/src/protocol.zig index 4ef0611..00a67c5 100644 --- a/src/protocol.zig +++ b/src/protocol.zig @@ -313,3 +313,179 @@ pub fn decodeUserList(allocator: Allocator, payload: []const u8) ![]authmod.User // --- 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 expire|ttl|persist +/// `/`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 ...` (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, +