mod claim;
mod group;
mod info;
mod setid;
use kevy_resp::{
ArgvView, encode_array_len, encode_bulk, encode_error, encode_integer, encode_null_bulk,
};
use kevy_store::{
EntryBatch, Store, StreamId, XAddIdSpec, now_unix_ms, parse_explicit_id, parse_range_end,
parse_range_start, parse_xadd_id,
};
pub(super) type StreamReply = (Vec<u8>, EntryBatch);
use crate::cmd::{store_err, wrong_args};
pub(crate) fn dispatch_stream<A: ArgvView + ?Sized>(
cmd: &[u8],
store: &mut Store,
args: &A,
out: &mut Vec<u8>,
) -> bool {
match cmd {
b"XADD" => cmd_xadd(store, args, out),
b"XLEN" => cmd_xlen(store, args, out),
b"XRANGE" => cmd_range(store, args, out, false),
b"XREVRANGE" => cmd_range(store, args, out, true),
b"XDEL" => cmd_xdel(store, args, out),
b"XTRIM" => cmd_xtrim(store, args, out),
b"XSETID" => setid::cmd_xsetid(store, args, out),
b"XREAD" => cmd_xread(store, args, out),
b"XGROUP" => group::cmd_xgroup(store, args, out),
b"XREADGROUP" => group::cmd_xreadgroup(store, args, out),
b"XACK" => group::cmd_xack(store, args, out),
b"XPENDING" => group::cmd_xpending(store, args, out),
b"XCLAIM" => claim::cmd_xclaim(store, args, out),
b"XAUTOCLAIM" => claim::cmd_xautoclaim(store, args, out),
b"XINFO" => info::cmd_xinfo(store, args, out),
_ => return false,
}
true
}
fn cmd_xadd<A: ArgvView + ?Sized>(store: &mut Store, args: &A, out: &mut Vec<u8>) {
if args.len() < 5 {
return wrong_args(out, "xadd");
}
let parsed = match parse_xadd_argv(args) {
Ok(p) => p,
Err(msg) => return encode_error(out, msg),
};
let id = match store.xadd(
&args[1],
parsed.id,
parsed.fields,
parsed.nomkstream,
now_unix_ms(),
) {
Ok(Some(id)) => id,
Ok(None) => return encode_null_bulk(out), Err(kevy_store::StoreError::OutOfRange) => {
return encode_error(
out,
"ERR The ID specified in XADD is equal or smaller than the target stream top item",
);
}
Err(e) => return store_err(out, e),
};
if let Some(trim) = parsed.trim {
apply_trim(store, &args[1], trim);
}
encode_bulk(out, &id.encode());
}
struct XAddParsed {
nomkstream: bool,
trim: Option<TrimSpec>,
id: XAddIdSpec,
fields: Vec<(Vec<u8>, Vec<u8>)>,
}
enum TrimSpec {
MaxLen(u64),
MinId(StreamId),
}
fn parse_xadd_argv<A: ArgvView + ?Sized>(args: &A) -> Result<XAddParsed, &'static str> {
let mut i = 2;
let mut nomkstream = false;
let mut trim: Option<TrimSpec> = None;
while i < args.len() {
let tok = args[i].to_ascii_uppercase();
match tok.as_slice() {
b"NOMKSTREAM" => {
nomkstream = true;
i += 1;
}
b"MAXLEN" => {
let (spec, used) = parse_trim_arg(args, i + 1, true)?;
trim = Some(spec);
i += 1 + used;
}
b"MINID" => {
let (spec, used) = parse_trim_arg(args, i + 1, false)?;
trim = Some(spec);
i += 1 + used;
}
_ => break,
}
}
if i + 2 >= args.len() {
return Err("ERR wrong number of arguments for 'xadd' command");
}
let id = parse_xadd_id(&args[i]).map_err(|_| {
"ERR Invalid stream ID specified as stream command argument"
})?;
i += 1;
let rest = args.len() - i;
if !rest.is_multiple_of(2) || rest == 0 {
return Err("ERR wrong number of arguments for 'xadd' command");
}
let mut fields = Vec::with_capacity(rest / 2);
while i < args.len() {
fields.push((args[i].to_vec(), args[i + 1].to_vec()));
i += 2;
}
Ok(XAddParsed { nomkstream, trim, id, fields })
}
fn parse_trim_arg<A: ArgvView + ?Sized>(
args: &A,
start: usize,
maxlen: bool,
) -> Result<(TrimSpec, usize), &'static str> {
let mut used = 0usize;
let mut idx = start;
if let Some(t) = args.get(idx)
&& (t == b"=" || t == b"~")
{
idx += 1;
used += 1;
}
let val = args.get(idx).ok_or("ERR syntax error")?;
used += 1;
if maxlen {
let n: u64 = std::str::from_utf8(val)
.ok()
.and_then(|s| s.parse().ok())
.ok_or("ERR value is not an integer or out of range")?;
Ok((TrimSpec::MaxLen(n), used))
} else {
let id = parse_explicit_id(val, false)
.map_err(|_| "ERR Invalid stream ID specified as stream command argument")?;
Ok((TrimSpec::MinId(id), used))
}
}
fn apply_trim(store: &mut Store, key: &[u8], trim: TrimSpec) {
let _ = match trim {
TrimSpec::MaxLen(n) => store.xtrim_maxlen(key, n),
TrimSpec::MinId(id) => store.xtrim_minid(key, id),
};
}
fn cmd_xlen<A: ArgvView + ?Sized>(store: &mut Store, args: &A, out: &mut Vec<u8>) {
if args.len() != 2 {
return wrong_args(out, "xlen");
}
match store.xlen(&args[1]) {
Ok(n) => encode_integer(out, n as i64),
Err(e) => store_err(out, e),
}
}
fn cmd_range<A: ArgvView + ?Sized>(
store: &mut Store,
args: &A,
out: &mut Vec<u8>,
rev: bool,
) {
if !(4..=6).contains(&args.len()) {
return wrong_args(out, if rev { "xrevrange" } else { "xrange" });
}
let (s_arg, e_arg) = if rev {
(&args[3], &args[2])
} else {
(&args[2], &args[3])
};
let Ok(start) = parse_range_start(s_arg) else {
return encode_error(
out,
"ERR Invalid stream ID specified as stream command argument",
);
};
let Ok(end) = parse_range_end(e_arg) else {
return encode_error(
out,
"ERR Invalid stream ID specified as stream command argument",
);
};
let count = match parse_optional_count(args, 4) {
Ok(c) => c,
Err(msg) => return encode_error(out, msg),
};
let entries = match (rev, store.xrange(&args[1], start, end, count)) {
(false, Ok(es)) => es,
(true, _) => match store.xrevrange(&args[1], start, end, count) {
Ok(es) => es,
Err(e) => return store_err(out, e),
},
(false, Err(e)) => return store_err(out, e),
};
emit_entries(out, &entries);
}
fn parse_optional_count<A: ArgvView + ?Sized>(
args: &A,
start: usize,
) -> Result<Option<usize>, &'static str> {
if start >= args.len() {
return Ok(None);
}
if !args[start].eq_ignore_ascii_case(b"COUNT") {
return Err("ERR syntax error");
}
let n = args.get(start + 1).ok_or("ERR syntax error")?;
let n: usize = std::str::from_utf8(n)
.ok()
.and_then(|s| s.parse().ok())
.ok_or("ERR value is not an integer or out of range")?;
Ok(Some(n))
}
fn cmd_xdel<A: ArgvView + ?Sized>(store: &mut Store, args: &A, out: &mut Vec<u8>) {
if args.len() < 3 {
return wrong_args(out, "xdel");
}
let mut ids = Vec::with_capacity(args.len() - 2);
for i in 2..args.len() {
match parse_explicit_id(&args[i], false) {
Ok(id) => ids.push(id),
Err(_) => {
return encode_error(
out,
"ERR Invalid stream ID specified as stream command argument",
);
}
}
}
match store.xdel(&args[1], &ids) {
Ok(n) => encode_integer(out, n as i64),
Err(e) => store_err(out, e),
}
}
fn cmd_xtrim<A: ArgvView + ?Sized>(store: &mut Store, args: &A, out: &mut Vec<u8>) {
if args.len() < 4 {
return wrong_args(out, "xtrim");
}
let mode = args[2].to_ascii_uppercase();
let spec = match mode.as_slice() {
b"MAXLEN" => match parse_trim_arg(args, 3, true) {
Ok((s, _)) => s,
Err(msg) => return encode_error(out, msg),
},
b"MINID" => match parse_trim_arg(args, 3, false) {
Ok((s, _)) => s,
Err(msg) => return encode_error(out, msg),
},
_ => return encode_error(out, "ERR syntax error"),
};
let n = match spec {
TrimSpec::MaxLen(n) => store.xtrim_maxlen(&args[1], n),
TrimSpec::MinId(id) => store.xtrim_minid(&args[1], id),
};
match n {
Ok(n) => encode_integer(out, n as i64),
Err(e) => store_err(out, e),
}
}
fn cmd_xread<A: ArgvView + ?Sized>(store: &mut Store, args: &A, out: &mut Vec<u8>) {
let parsed = match parse_xread_argv(args) {
Ok(p) => p,
Err(msg) => return encode_error(out, msg),
};
let blocking = parsed.block_ms.is_some();
let mut reply: Vec<StreamReply> = Vec::new();
for (key, last_seen_arg) in parsed.streams {
let last_seen = if last_seen_arg == b"$" {
match store.xread_dollar_last_id(&key) {
Ok(id) => id,
Err(e) => return store_err(out, e),
}
} else {
match parse_explicit_id(&last_seen_arg, false) {
Ok(id) => id,
Err(_) => {
return encode_error(
out,
"ERR Invalid stream ID specified as stream command argument",
);
}
}
};
let entries = match store.xread(&key, last_seen, parsed.count) {
Ok(es) => es,
Err(e) => return store_err(out, e),
};
if !entries.is_empty() {
reply.push((key, entries));
}
}
if reply.is_empty() && blocking {
return;
}
emit_xread_reply(out, &reply);
}
struct XReadParsed {
count: Option<usize>,
block_ms: Option<u64>,
streams: Vec<(Vec<u8>, Vec<u8>)>, }
fn parse_xread_argv<A: ArgvView + ?Sized>(args: &A) -> Result<XReadParsed, &'static str> {
let mut count: Option<usize> = None;
let mut block_ms: Option<u64> = None;
let mut i = 1;
while i < args.len() {
let tok = args[i].to_ascii_uppercase();
match tok.as_slice() {
b"COUNT" => {
count = Some(xread_parse_kv_usize(
args,
i + 1,
"ERR value is not an integer or out of range",
)?);
i += 2;
}
b"BLOCK" => {
block_ms = Some(xread_parse_kv_u64(
args,
i + 1,
"ERR timeout is not an integer or out of range",
)?);
i += 2;
}
b"STREAMS" => {
let streams = xread_parse_streams(args, i + 1)?;
return Ok(XReadParsed {
count,
block_ms,
streams,
});
}
_ => return Err("ERR syntax error"),
}
}
Err("ERR syntax error")
}
fn xread_parse_kv_usize<A: ArgvView + ?Sized>(
args: &A,
idx: usize,
bad: &'static str,
) -> Result<usize, &'static str> {
let n = args.get(idx).ok_or("ERR syntax error")?;
std::str::from_utf8(n)
.ok()
.and_then(|s| s.parse().ok())
.ok_or(bad)
}
fn xread_parse_kv_u64<A: ArgvView + ?Sized>(
args: &A,
idx: usize,
bad: &'static str,
) -> Result<u64, &'static str> {
let n = args.get(idx).ok_or("ERR syntax error")?;
std::str::from_utf8(n)
.ok()
.and_then(|s| s.parse().ok())
.ok_or(bad)
}
type StreamKeyLastSeen = (Vec<u8>, Vec<u8>);
fn xread_parse_streams<A: ArgvView + ?Sized>(
args: &A,
start: usize,
) -> Result<Vec<StreamKeyLastSeen>, &'static str> {
let rest = args.len() - start;
if rest == 0 || !rest.is_multiple_of(2) {
return Err(
"ERR Unbalanced XREAD list of streams: for each stream key an ID or '$' must be specified.",
);
}
let n = rest / 2;
let mut streams = Vec::with_capacity(n);
for k in 0..n {
streams.push((args[start + k].to_vec(), args[start + n + k].to_vec()));
}
Ok(streams)
}
pub(super) fn emit_entries(out: &mut Vec<u8>, entries: &EntryBatch) {
encode_array_len(out, entries.len() as i64);
for (id, fv) in entries {
encode_array_len(out, 2);
encode_bulk(out, &id.encode());
encode_array_len(out, (fv.len() * 2) as i64);
for (f, v) in fv {
encode_bulk(out, f);
encode_bulk(out, v);
}
}
}
fn emit_xread_reply(out: &mut Vec<u8>, reply: &[StreamReply]) {
if reply.is_empty() {
encode_array_len(out, -1);
return;
}
encode_array_len(out, reply.len() as i64);
for (key, entries) in reply {
encode_array_len(out, 2);
encode_bulk(out, key);
emit_entries(out, entries);
}
}