//! Redis list commands //! //! Lists are linked lists of strings, optimized for pushing and popping //! at both ends. They support blocking operations for queue patterns. //! //! ## Use Cases //! //! - Message queues: LPUSH + BRPOP //! - Activity feeds: LPUSH + LRANGE //! - Bounded collections: LPUSH + LTRIM //! - Background job queues: RPUSH + BLPOP //! //! ## Performance //! //! - Push/pop at ends: O(1) //! - Access by index: O(n) where n is distance from nearest end //! - Range queries: O(s+n) where s is start offset and n is elements returned //! //! ## Examples //! //! ```zig //! // Simple queue //! _ = try client.lists().rpush("queue", &.{"job1", "job2"}); //! const job = try client.lists().lpop("queue"); // "job1" //! //! // Recent items with bounded size //! _ = try client.lists().lpush("recent", &.{item}); //! try client.lists().ltrim("recent", 0, 99); // keep only 100 most recent //! //! // Blocking pop (for workers) //! const item = try client.lists().blpop(&.{"queue"}, 30); // wait up to 30s //! ``` const std = @import("std"); const Client = @import("../client.zig").Client; const Value = @import("../resp.zig").Value; const CommandError = @import("../resp.zig").CommandError; const ClientError = @import("../resp.zig").ClientError; /// List command implementations. pub const ListCommands = struct { client: *Client, pub fn init(client: *Client) ListCommands { return .{ .client = client }; } // ======================================================================== // Push Operations // ======================================================================== /// LPUSH key value [value ...] - prepend to list /// Returns new list length. pub fn lpush(self: *ListCommands, key: []const u8, values: []const []const u8) ClientError!i64 { var args = try self.client.allocator.alloc([]const u8, values.len + 2); defer self.client.allocator.free(args); args[0] = "LPUSH"; args[1] = key; @memcpy(args[2..], values); const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => 0, }; } /// RPUSH key value [value ...] - append to list /// Returns new list length. pub fn rpush(self: *ListCommands, key: []const u8, values: []const []const u8) ClientError!i64 { var args = try self.client.allocator.alloc([]const u8, values.len + 2); defer self.client.allocator.free(args); args[0] = "RPUSH"; args[1] = key; @memcpy(args[2..], values); const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => 0, }; } /// LPUSHX key value - prepend only if list exists pub fn lpushx(self: *ListCommands, key: []const u8, value: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "LPUSHX", key, value }); return switch (result) { .integer => |i| i, else => 0, }; } /// RPUSHX key value - append only if list exists pub fn rpushx(self: *ListCommands, key: []const u8, value: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "RPUSHX", key, value }); return switch (result) { .integer => |i| i, else => 0, }; } // ======================================================================== // Pop Operations // ======================================================================== /// LPOP key [count] - remove and return first element(s) pub fn lpop(self: *ListCommands, key: []const u8) ClientError!?[]const u8 { const result = try self.client.sendCommand(&.{ "LPOP", key }); return switch (result) { .bulk => |b| b, .nil => null, else => null, }; } /// LPOP key count - pop multiple elements pub fn lpopCount(self: *ListCommands, key: []const u8, count: u32) ClientError![]const Value { var buf: [16]u8 = undefined; const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable; const result = try self.client.sendCommand(&.{ "LPOP", key, count_str }); return switch (result) { .array => |a| a, .nil => &.{}, else => &.{}, }; } /// RPOP key - remove and return last element pub fn rpop(self: *ListCommands, key: []const u8) ClientError!?[]const u8 { const result = try self.client.sendCommand(&.{ "RPOP", key }); return switch (result) { .bulk => |b| b, .nil => null, else => null, }; } /// RPOP key count - pop multiple elements from end pub fn rpopCount(self: *ListCommands, key: []const u8, count: u32) ClientError![]const Value { var buf: [16]u8 = undefined; const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable; const result = try self.client.sendCommand(&.{ "RPOP", key, count_str }); return switch (result) { .array => |a| a, .nil => &.{}, else => &.{}, }; } // ======================================================================== // Blocking Pop Operations // ======================================================================== /// Result from blocking pop operations pub const BlockingPopResult = struct { key: []const u8, value: []const u8, }; /// BLPOP key [key ...] timeout - blocking left pop /// Timeout in seconds (0 = block forever). Returns null on timeout. pub fn blpop(self: *ListCommands, keys: []const []const u8, timeout: f64) ClientError!?BlockingPopResult { var args = try self.client.allocator.alloc([]const u8, keys.len + 2); defer self.client.allocator.free(args); args[0] = "BLPOP"; @memcpy(args[1..][0..keys.len], keys); var buf: [32]u8 = undefined; args[keys.len + 1] = std.fmt.bufPrint(&buf, "{d}", .{timeout}) catch unreachable; const result = try self.client.sendCommand(args); return switch (result) { .array => |arr| { if (arr.len < 2) return null; const k = arr[0].asString() orelse return null; const v = arr[1].asString() orelse return null; return BlockingPopResult{ .key = k, .value = v }; }, .nil => null, else => null, }; } /// BRPOP key [key ...] timeout - blocking right pop pub fn brpop(self: *ListCommands, keys: []const []const u8, timeout: f64) ClientError!?BlockingPopResult { var args = try self.client.allocator.alloc([]const u8, keys.len + 2); defer self.client.allocator.free(args); args[0] = "BRPOP"; @memcpy(args[1..][0..keys.len], keys); var buf: [32]u8 = undefined; args[keys.len + 1] = std.fmt.bufPrint(&buf, "{d}", .{timeout}) catch unreachable; const result = try self.client.sendCommand(args); return switch (result) { .array => |arr| { if (arr.len < 2) return null; const k = arr[0].asString() orelse return null; const v = arr[1].asString() orelse return null; return BlockingPopResult{ .key = k, .value = v }; }, .nil => null, else => null, }; } /// LMPOP numkeys key [key ...] LEFT|RIGHT [COUNT count] (Redis 7+) pub fn lmpop(self: *ListCommands, keys: []const []const u8, direction: enum { left, right }, count: ?u32) ClientError!?struct { key: []const u8, values: []const Value } { const base_args = 3 + keys.len + if (count != null) @as(usize, 2) else @as(usize, 0); var args = try self.client.allocator.alloc([]const u8, base_args); defer self.client.allocator.free(args); var numkeys_buf: [16]u8 = undefined; args[0] = "LMPOP"; args[1] = std.fmt.bufPrint(&numkeys_buf, "{d}", .{keys.len}) catch unreachable; @memcpy(args[2..][0..keys.len], keys); args[2 + keys.len] = if (direction == .left) "LEFT" else "RIGHT"; const arg_idx = 3 + keys.len; var count_buf: [16]u8 = undefined; if (count) |c| { args[arg_idx] = "COUNT"; args[arg_idx + 1] = std.fmt.bufPrint(&count_buf, "{d}", .{c}) catch unreachable; } const result = try self.client.sendCommand(args[0..arg_idx]); return switch (result) { .array => |arr| { if (arr.len < 2) return null; const key = arr[0].asString() orelse return null; const values = arr[1].asArray() orelse return null; return .{ .key = key, .values = values }; }, .nil => null, else => null, }; } // ======================================================================== // Access Operations // ======================================================================== /// LINDEX key index - get element by index pub fn lindex(self: *ListCommands, key: []const u8, index: i64) ClientError!?[]const u8 { var buf: [24]u8 = undefined; const idx_str = std.fmt.bufPrint(&buf, "{d}", .{index}) catch unreachable; const result = try self.client.sendCommand(&.{ "LINDEX", key, idx_str }); return switch (result) { .bulk => |b| b, else => null, }; } /// LRANGE key start stop - get range of elements /// Supports negative indices (-1 = last element). pub fn lrange(self: *ListCommands, key: []const u8, start: i64, stop: i64) ClientError![]const Value { var start_buf: [24]u8 = undefined; var stop_buf: [24]u8 = undefined; const start_str = std.fmt.bufPrint(&start_buf, "{d}", .{start}) catch unreachable; const stop_str = std.fmt.bufPrint(&stop_buf, "{d}", .{stop}) catch unreachable; const result = try self.client.sendCommand(&.{ "LRANGE", key, start_str, stop_str }); return switch (result) { .array => |a| a, else => &.{}, }; } /// LLEN key - get list length pub fn llen(self: *ListCommands, key: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "LLEN", key }); return switch (result) { .integer => |i| i, else => 0, }; } // ======================================================================== // Modification Operations // ======================================================================== /// LSET key index value - set element at index pub fn lset(self: *ListCommands, key: []const u8, index: i64, value: []const u8) ClientError!void { var buf: [24]u8 = undefined; const idx_str = std.fmt.bufPrint(&buf, "{d}", .{index}) catch unreachable; const result = try self.client.sendCommand(&.{ "LSET", key, idx_str, value }); if (result.isError()) return CommandError.RedisError; } /// LTRIM key start stop - trim list to specified range pub fn ltrim(self: *ListCommands, key: []const u8, start: i64, stop: i64) ClientError!void { var start_buf: [24]u8 = undefined; var stop_buf: [24]u8 = undefined; const start_str = std.fmt.bufPrint(&start_buf, "{d}", .{start}) catch unreachable; const stop_str = std.fmt.bufPrint(&stop_buf, "{d}", .{stop}) catch unreachable; const result = try self.client.sendCommand(&.{ "LTRIM", key, start_str, stop_str }); if (result.isError()) return CommandError.RedisError; } /// LREM key count value - remove elements /// count > 0: remove first count matches from head /// count < 0: remove first |count| matches from tail /// count = 0: remove all matches pub fn lrem(self: *ListCommands, key: []const u8, count: i64, value: []const u8) ClientError!i64 { var buf: [24]u8 = undefined; const count_str = std.fmt.bufPrint(&buf, "{d}", .{count}) catch unreachable; const result = try self.client.sendCommand(&.{ "LREM", key, count_str, value }); return switch (result) { .integer => |i| i, else => 0, }; } /// LINSERT key BEFORE|AFTER pivot value - insert before/after element pub fn linsert(self: *ListCommands, key: []const u8, position: enum { before, after }, pivot: []const u8, value: []const u8) ClientError!i64 { const pos_str = if (position == .before) "BEFORE" else "AFTER"; const result = try self.client.sendCommand(&.{ "LINSERT", key, pos_str, pivot, value }); return switch (result) { .integer => |i| i, else => -1, }; } /// LPOS key element [RANK rank] [COUNT count] [MAXLEN len] - find element position (Redis 6.0.6+) pub fn lpos(self: *ListCommands, key: []const u8, element: []const u8) ClientError!?i64 { const result = try self.client.sendCommand(&.{ "LPOS", key, element }); return switch (result) { .integer => |i| i, .nil => null, else => null, }; } /// LMOVE source destination LEFT|RIGHT LEFT|RIGHT - atomically move element pub fn lmove(self: *ListCommands, source: []const u8, dest: []const u8, src_dir: enum { left, right }, dst_dir: enum { left, right }) ClientError!?[]const u8 { const src_str = if (src_dir == .left) "LEFT" else "RIGHT"; const dst_str = if (dst_dir == .left) "LEFT" else "RIGHT"; const result = try self.client.sendCommand(&.{ "LMOVE", source, dest, src_str, dst_str }); return switch (result) { .bulk => |b| b, else => null, }; } }; /// Extend Client with list commands. pub fn lists(client: *Client) ListCommands { return ListCommands.init(client); }