using System.Text;
namespace Firefly
{
public partial class Firefly
{
#region List Operations
///
/// Handles the LPUSH command which adds an element to the left of a list.
///
/// Command arguments in format: "key value"
/// The length of the list after the push operation
static byte[] HandleLPushCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpush' command\r\n");
}
string key = parts[0];
string value = parts[1];
try
{
// Use helper function to safely modify the list with write lock
int newLength = 0;
ListStoreWithWriteLock(key, list =>
{
list.Insert(0, value);
newLength = list.Count;
});
return Encoding.UTF8.GetBytes($":{newLength}\r\n");
}
catch (InvalidOperationException ex)
{
// Handle the case where the key already exists with a different type
string? existingType = GetKeyType(key);
if (existingType != null)
{
return Encoding.UTF8.GetBytes($"-ERR key '{key}' already exists as type '{existingType}'\r\n");
}
return Encoding.UTF8.GetBytes($"-ERR {ex.Message}\r\n");
}
}
///
/// Handles the RPUSH command which adds values to the tail of a list.
///
/// Command arguments in format: "key value1 [value2 ...]"
/// Response indicating the new length of the list
static byte[] HandleRPushCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'rpush' command\r\n");
}
string key = parts[0];
List result = [];
// Use helper function to safely modify the list with write lock
ListStoreWithWriteLock(key, list =>
{
// Add all values to the end of the list
for (int i = 1; i < parts.Length; i++)
{
list.Add(parts[i]);
}
result.Add(list.Count.ToString());
});
return Encoding.UTF8.GetBytes($":{result[0]}\r\n");
}
///
/// Handles the LPOP command which removes and returns the first element of a list.
///
/// Command arguments containing the key
/// The popped value or nil if the list is empty
static byte[] HandleLPopCommand(string args)
{
if (string.IsNullOrWhiteSpace(args))
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpop' command\r\n");
}
string key = args.Trim();
List result = [];
// Use helper function to safely modify the list with write lock
ListStoreWithWriteLock(key, list =>
{
if (list.Count > 0)
{
// Remove and store the first element
string value = list[0];
list.RemoveAt(0);
result.Add(value);
// Clean up empty lists
if (list.Count == 0)
{
ListStoreRemove(key);
}
}
});
return result.Count > 0
? Encoding.UTF8.GetBytes($"+{result[0]}\r\n")
: Encoding.UTF8.GetBytes("$-1\r\n");
}
///
/// Handles the RPOP command which removes and returns the last element of a list.
///
/// Command arguments containing the key
/// The popped value or nil if the list is empty
static byte[] HandleRPopCommand(string args)
{
if (string.IsNullOrWhiteSpace(args))
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'rpop' command\r\n");
}
string key = args.Trim();
List result = [];
// Use helper function to safely modify the list with write lock
ListStoreWithWriteLock(key, list =>
{
if (list.Count > 0)
{
// Remove and store the last element
int lastIndex = list.Count - 1;
string value = list[lastIndex];
list.RemoveAt(lastIndex);
result.Add(value);
// Clean up empty lists
if (list.Count == 0)
{
ListStoreRemove(key);
}
}
});
return result.Count > 0
? Encoding.UTF8.GetBytes($"+{result[0]}\r\n")
: Encoding.UTF8.GetBytes("$-1\r\n");
}
///
/// Handles the LLEN command which returns the length of a list.
///
/// Command arguments containing the key
/// The length of the list or 0 if the key does not exist
static byte[] HandleLLengthCommand(string args)
{
if (string.IsNullOrWhiteSpace(args))
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'llen' command\r\n");
}
string key = args.Trim();
int length = 0;
// Use helper function to safely read the list with read lock
ListStoreWithReadLock(key, list =>
{
length = list.Count;
});
return Encoding.UTF8.GetBytes($":{length}\r\n");
}
///
/// Handles the LRANGE command which returns a range of elements from a list.
///
/// Command arguments in format: "key start stop"
/// Array of elements in the specified range
static byte[] HandleLRangeCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lrange' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int start) || !int.TryParse(parts[2], out int stop))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
// Use helper function to safely read the list with read lock
return ListStoreWithReadLock(key, list =>
{
if (list.Count == 0)
{
return Encoding.UTF8.GetBytes("*\r\n");
}
// Handle negative indices (counting from the end)
if (start < 0) start = list.Count + start;
if (stop < 0) stop = list.Count + stop;
// Ensure indices are within bounds
start = Math.Max(start, 0);
stop = Math.Min(stop, list.Count - 1);
if (start > stop)
{
return Encoding.UTF8.GetBytes("*\r\n");
}
// Build response with all elements in range
StringBuilder response = new();
response.Append("*\r\n");
for (int i = start; i <= stop; i++)
{
response.Append($"+{list[i]}\r\n");
}
return Encoding.UTF8.GetBytes(response.ToString());
}) ?? Encoding.UTF8.GetBytes("*\r\n");
}
///
/// Handles the LINDEX command which returns an element from a list by its index.
///
/// Command arguments in format: "key index"
/// The element at the specified index or nil if not found
static byte[] HandleLIndexCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lindex' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int index))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
// Use helper function to safely read the list with read lock
return ListStoreWithReadLock(key, list =>
{
if (list.Count == 0)
{
return Encoding.UTF8.GetBytes("$-1\r\n");
}
// Handle negative index (counting from the end)
if (index < 0)
{
index = list.Count + index;
}
// Check if index is within bounds
if (index < 0 || index >= list.Count)
{
return Encoding.UTF8.GetBytes("$-1\r\n");
}
return Encoding.UTF8.GetBytes($"+{list[index]}\r\n");
}) ?? Encoding.UTF8.GetBytes("$-1\r\n");
}
///
/// Handles the LSET command which sets the value of an element in a list by its index.
///
/// Command arguments in format: "key index value"
/// OK on success, error if index is out of range
static byte[] HandleLSetCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lset' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int index))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
string value = parts[2];
bool success = false;
// Use helper function to safely modify the list with write lock
ListStoreWithWriteLock(key, list =>
{
if (list.Count == 0)
{
return;
}
// Handle negative index (counting from the end)
if (index < 0)
{
index = list.Count + index;
}
// Check if index is within bounds
if (index < 0 || index >= list.Count)
{
return;
}
list[index] = value;
success = true;
});
return success
? Encoding.UTF8.GetBytes("+OK\r\n")
: Encoding.UTF8.GetBytes("-ERR index out of range\r\n");
}
///
/// Handles the LPOS command which returns the position of an element in a list.
///
/// Command arguments in format: "key element [RANK rank] [MAXLEN len]"
/// The position of the element or nil if not found
static byte[] HandleLPosCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpos' command\r\n");
}
string key = parts[0];
string element = parts[1];
// Optional parameters
int rank = 1; // Default to first occurrence
int maxlen = 0; // 0 means no limit
// Parse optional parameters
for (int i = 2; i < parts.Length; i++)
{
if (parts[i].Equals("RANK", StringComparison.OrdinalIgnoreCase) && i + 1 < parts.Length)
{
if (int.TryParse(parts[++i], out int parsedRank))
{
rank = parsedRank;
}
}
else if (parts[i].Equals("MAXLEN", StringComparison.OrdinalIgnoreCase) && i + 1 < parts.Length)
{
if (int.TryParse(parts[++i], out int parsedMaxlen))
{
maxlen = parsedMaxlen;
}
}
}
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterReadLock();
try
{
if (!listStoreShards[shardIndex].TryGetValue(key, out List? list) || list == null)
{
return Encoding.UTF8.GetBytes("$-1\r\n"); // Key doesn't exist
}
int found = 0;
for (int i = 0; i < list.Count; i++)
{
if (maxlen > 0 && i >= maxlen)
{
break;
}
if (list[i] == element)
{
found++;
if (found == Math.Abs(rank))
{
return Encoding.UTF8.GetBytes($":{i}\r\n");
}
}
}
return Encoding.UTF8.GetBytes("$-1\r\n"); // Element not found
}
finally
{
listStoreLocks[shardIndex].ExitReadLock();
}
}
///
/// Handles the LTRIM command which trims a list to the specified range.
///
/// Command arguments in format: "key start stop"
/// OK on success, error if arguments are invalid
static byte[] HandleLTrimCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'ltrim' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int start) || !int.TryParse(parts[2], out int stop))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterWriteLock();
try
{
if (!listStoreShards[shardIndex].TryGetValue(key, out List? list) || list == null)
{
return Encoding.UTF8.GetBytes("+OK\r\n"); // Non-existent key is treated as empty list
}
// Handle negative indices
if (start < 0) start = list.Count + start;
if (stop < 0) stop = list.Count + stop;
// Normalize boundaries
start = Math.Max(start, 0);
stop = Math.Min(stop, list.Count - 1);
if (start > stop || start >= list.Count)
{
// Clear the list if range is invalid
list.Clear();
listStoreShards[shardIndex].TryRemove(key, out _);
}
else
{
// Calculate the new range
int newLength = stop - start + 1;
if (start > 0 || stop < list.Count - 1)
{
var trimmed = list.GetRange(start, newLength);
list.Clear();
list.AddRange(trimmed);
}
}
return Encoding.UTF8.GetBytes("+OK\r\n");
}
finally
{
listStoreLocks[shardIndex].ExitWriteLock();
}
}
///
/// Handles the LREM command which removes elements equal to the given value from a list.
///
/// Command arguments in format: "key count element"
/// The number of removed elements
static byte[] HandleLRemCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lrem' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int count))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
string element = parts[2];
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterWriteLock();
try
{
if (!listStoreShards[shardIndex].TryGetValue(key, out List? list) || list == null)
{
return Encoding.UTF8.GetBytes(":0\r\n"); // Key doesn't exist
}
int removed = 0;
if (count > 0)
{
// Remove count occurrences from head to tail
for (int i = 0; i < list.Count && removed < count; i++)
{
if (list[i] == element)
{
list.RemoveAt(i);
removed++;
i--; // Adjust index after removal
}
}
}
else if (count < 0)
{
// Remove |count| occurrences from tail to head
for (int i = list.Count - 1; i >= 0 && removed < -count; i--)
{
if (list[i] == element)
{
list.RemoveAt(i);
removed++;
}
}
}
else // count == 0
{
// Remove all occurrences
removed = list.RemoveAll(x => x == element);
}
// Remove the key if the list is empty
if (list.Count == 0)
{
listStoreShards[shardIndex].TryRemove(key, out _);
}
return Encoding.UTF8.GetBytes($":{removed}\r\n");
}
finally
{
listStoreLocks[shardIndex].ExitWriteLock();
}
}
#region List Store Helpers
///
/// Gets or creates a list for a given key.
///
/// The key to get or create the list for
/// The list
private static List GetOrCreateList(string key)
{
int shardIndex = GetShardIndex(key);
// If the key doesn't exist in this shard, check if it exists in any other store
if (!listStoreShards[shardIndex].ContainsKey(key))
{
// Check if key exists in any other store
if (!EnsureKeyDoesNotExist(key, "list"))
{
throw new InvalidOperationException($"Key '{key}' already exists with a different type");
}
}
return listStoreShards[shardIndex].GetOrAdd(key, _ => []);
}
///
/// Checks if a list exists for a given key.
///
/// The key to check the list for
/// True if the list exists, false otherwise
private static bool ListStoreExists(string key)
{
int shardIndex = GetShardIndex(key);
return listStoreShards[shardIndex].ContainsKey(key);
}
///
/// Gets a list for a given key.
///
/// The key to get the list for
/// The list
/// True if the list was found, false otherwise
private static bool ListStoreGet(string key, out List? value)
{
int shardIndex = GetShardIndex(key);
return listStoreShards[shardIndex].TryGetValue(key, out value);
}
///
/// Removes a list for a given key.
///
/// The key to remove the list for
/// True if the list was removed, false otherwise
private static bool ListStoreRemove(string key)
{
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterWriteLock();
try
{
return listStoreShards[shardIndex].TryRemove(key, out _);
}
finally
{
listStoreLocks[shardIndex].ExitWriteLock();
}
}
///
/// Executes an action with a write lock on a list for a given key.
///
/// The key to execute the action on
/// The action to execute
private static void ListStoreWithWriteLock(string key, Action> action)
{
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterWriteLock();
try
{
var list = GetOrCreateList(key);
action(list);
}
finally
{
listStoreLocks[shardIndex].ExitWriteLock();
}
}
///
/// Executes an action with a read lock on a list for a given key.
///
/// The type of the result
/// The key to execute the action on
/// The action to execute
private static T ListStoreWithReadLock(string key, Func, T> action)
{
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterReadLock();
try
{
if (ListStoreGet(key, out List? list) && list != null)
{
return action(list);
}
return default!;
}
finally
{
listStoreLocks[shardIndex].ExitReadLock();
}
}
#endregion
#endregion
}
}