#[cfg(feature = "protocol-http")]
#[cfg_attr(docsrs, doc(cfg(feature = "protocol-http")))]
pub mod http;
#[cfg(feature = "protocol-ws")]
#[cfg_attr(docsrs, doc(cfg(feature = "protocol-ws")))]
pub mod ws;
use surrealdb_core::iam::token::Token;
use uuid::Uuid;
use crate::conn::cmd::Command;
use crate::types::{Array, SurrealValue, Value};
#[derive(Clone, Debug, SurrealValue)]
#[surreal(crate = "crate::types")]
pub(crate) struct RouterRequest {
pub(crate) id: Option<i64>,
pub(crate) method: &'static str,
pub(crate) params: Option<Value>,
pub(crate) txn: Option<Uuid>,
#[surreal(rename = "session")]
pub(crate) session_id: Option<Uuid>,
}
impl Command {
fn into_router_request(
self,
id: Option<i64>,
session_id: Option<Uuid>,
) -> Option<RouterRequest> {
use crate::types::Uuid;
let res = match self {
Command::Use {
namespace,
database,
} => {
let namespace = namespace.map(Value::String).unwrap_or(Value::None);
let database = database.map(Value::String).unwrap_or(Value::None);
RouterRequest {
id,
method: "use",
params: Some(Value::Array(Array::from(vec![namespace, database]))),
txn: None,
session_id,
}
}
Command::Signup {
credentials,
} => RouterRequest {
id,
method: "signup",
params: Some(Value::Array(Array::from(vec![Value::from_t(credentials)]))),
txn: None,
session_id,
},
Command::Signin {
credentials,
} => RouterRequest {
id,
method: "signin",
params: Some(Value::Array(Array::from(vec![Value::from_t(credentials)]))),
txn: None,
session_id,
},
Command::Authenticate {
token,
} => RouterRequest {
id,
method: "authenticate",
params: Some(Value::Array(Array::from(vec![match token {
Token::Access(access) => access.into_value(),
Token::WithRefresh {
access,
..
} => access.into_value(),
}]))),
txn: None,
session_id,
},
Command::Refresh {
token,
} => RouterRequest {
id,
method: "refresh",
params: Some(Value::Array(Array::from(vec![Value::from_t(token)]))),
txn: None,
session_id,
},
Command::Invalidate => RouterRequest {
id,
method: "invalidate",
params: None,
txn: None,
session_id,
},
Command::Begin => RouterRequest {
id,
method: "begin",
params: None,
txn: None,
session_id,
},
Command::Commit {
txn,
} => RouterRequest {
id,
method: "commit",
params: Some(Value::Array(Array::from(vec![Value::Uuid(Uuid::from(txn))]))),
txn: None,
session_id,
},
Command::Rollback {
txn,
} => RouterRequest {
id,
method: "cancel",
params: Some(Value::Array(Array::from(vec![Value::Uuid(Uuid::from(txn))]))),
txn: None,
session_id,
},
Command::Revoke {
token,
} => RouterRequest {
id,
method: "revoke",
params: Some(Value::Array(Array::from(vec![token.into_value()]))),
txn: None,
session_id,
},
Command::Query {
txn,
query,
variables,
} => {
let params: Vec<Value> =
vec![Value::String(query.into_owned()), Value::Object(variables.into())];
RouterRequest {
id,
method: "query",
params: Some(Value::Array(Array::from(params))),
txn,
session_id,
}
}
Command::ExportFile {
..
}
| Command::ExportBytes {
..
}
| Command::ImportFile {
..
}
| Command::ExportBytesMl {
..
}
| Command::ExportMl {
..
}
| Command::ImportMl {
..
} => return None,
Command::Health => RouterRequest {
id,
method: "ping",
params: None,
txn: None,
session_id,
},
Command::Version => RouterRequest {
id,
method: "version",
params: None,
txn: None,
session_id,
},
Command::Set {
key,
value,
} => RouterRequest {
id,
method: "let",
params: Some(Value::from_t(vec![Value::from_t(key), value])),
txn: None,
session_id,
},
Command::Unset {
key,
} => RouterRequest {
id,
method: "unset",
params: Some(Value::from_t(vec![Value::from_t(key)])),
txn: None,
session_id,
},
Command::SubscribeLive {
..
} => return None,
Command::Kill {
uuid,
} => RouterRequest {
id,
method: "kill",
params: Some(Value::from_t(vec![Value::Uuid(Uuid::from(uuid))])),
txn: None,
session_id,
},
Command::Attach {
session_id,
} => RouterRequest {
id,
method: "attach",
params: None,
txn: None,
session_id: Some(session_id),
},
Command::Detach {
session_id,
} => RouterRequest {
id,
method: "detach",
params: None,
txn: None,
session_id: Some(session_id),
},
Command::Run {
name,
version,
args,
} => {
let version = version.map(Value::String).unwrap_or(Value::None);
RouterRequest {
id,
method: "run",
params: Some(Value::Array(Array::from(vec![
Value::String(name),
version,
Value::Array(args),
]))),
txn: None,
session_id,
}
}
};
Some(res)
}
fn replayable(&self) -> bool {
matches!(
self,
Command::Signup { .. }
| Command::Signin { .. }
| Command::Authenticate { .. }
| Command::Invalidate
| Command::Use { .. }
| Command::Set { .. }
| Command::Unset { .. }
)
}
#[cfg(feature = "protocol-ws")]
fn is_replay_noop_after(&self, prev: &Command) -> bool {
match (prev, self) {
(
Command::Use {
namespace: pn,
database: pd,
},
Command::Use {
namespace: nn,
database: nd,
},
) => {
let ns_noop = nn.is_none() || nn == pn;
let db_noop = nd.is_none() || nd == pd;
ns_noop && db_noop
}
_ => false,
}
}
}
#[cfg(feature = "protocol-ws")]
fn record_replayable(replay: &boxcar::Vec<Command>, command: Command) {
debug_assert!(
command.replayable(),
"record_replayable called with non-replayable command: {command:?}",
);
let n = replay.count();
if n > 0
&& let Some(prev) = replay.get(n - 1)
&& command.is_replay_noop_after(prev)
{
return;
}
replay.push(command);
}
#[cfg(test)]
mod test {
use uuid::Uuid;
use super::RouterRequest;
use crate::types::{Array, Number, SurrealValue, Value};
fn assert_converts<S, D, I>(req: &RouterRequest, s: S, d: D)
where
S: FnOnce(&Value) -> I,
D: FnOnce(I) -> Value,
{
let v = req.clone().into_value();
let ser = s(&v);
let val = d(ser);
let Value::Object(obj) = val else {
panic!("not an object");
};
assert_eq!(
obj.get("id").cloned().and_then(|x| if let Value::Number(Number::Int(x)) = x {
Some(x)
} else {
None
}),
req.id
);
let Some(Value::String(x)) = obj.get("method") else {
panic!("invalid method field: {obj:?}")
};
assert_eq!(x.as_str(), req.method);
assert_eq!(obj.get("params").cloned(), req.params);
}
#[test]
fn router_request_value_conversion() {
let request = RouterRequest {
id: Some(1234),
method: "request",
params: Some(Value::Array(Array::from(vec![
Value::Number(Number::Int(1234i64)),
Value::String("request".to_string()),
]))),
txn: Some(Uuid::new_v4()),
session_id: Some(Uuid::new_v4()),
};
assert_converts(
&request,
|i| surrealdb_core::rpc::format::flatbuffers::encode(i).unwrap(),
|b| surrealdb_core::rpc::format::flatbuffers::decode(&b).unwrap(),
);
}
}
#[cfg(all(test, feature = "protocol-ws"))]
mod replay_test {
use super::{Command, record_replayable};
fn use_cmd(ns: Option<&str>, db: Option<&str>) -> Command {
Command::Use {
namespace: ns.map(String::from),
database: db.map(String::from),
}
}
#[test]
fn record_replayable_dedups_identical_consecutive_use() {
let replay = boxcar::Vec::new();
record_replayable(&replay, use_cmd(Some("ns1"), None));
for _ in 0..500 {
record_replayable(&replay, use_cmd(None, Some("db1")));
}
assert_eq!(replay.count(), 2);
}
#[test]
fn record_replayable_keeps_distinct_use_entries() {
let replay = boxcar::Vec::new();
record_replayable(&replay, use_cmd(Some("ns1"), None));
record_replayable(&replay, use_cmd(None, Some("db1")));
record_replayable(&replay, use_cmd(Some("ns2"), None));
record_replayable(&replay, use_cmd(None, Some("db2")));
assert_eq!(replay.count(), 4);
}
#[test]
fn record_replayable_does_not_dedup_other_commands() {
let replay = boxcar::Vec::new();
record_replayable(&replay, Command::Invalidate);
record_replayable(&replay, Command::Invalidate);
assert_eq!(replay.count(), 2);
}
#[test]
fn record_replayable_does_not_skip_use_after_non_use_tail() {
let replay = boxcar::Vec::new();
record_replayable(&replay, use_cmd(Some("ns1"), Some("db1")));
record_replayable(&replay, Command::Invalidate);
record_replayable(&replay, use_cmd(Some("ns1"), Some("db1")));
assert_eq!(replay.count(), 3);
}
#[test]
fn is_replay_noop_after_partial_use_against_full_tail() {
let full = use_cmd(Some("ns1"), Some("db1"));
assert!(use_cmd(None, Some("db1")).is_replay_noop_after(&full));
assert!(!use_cmd(None, Some("db2")).is_replay_noop_after(&full));
}
#[test]
fn use_defaults_subsumed_by_prior_full_use_is_safe_to_drop() {
let full = use_cmd(Some("ns1"), Some("db1"));
assert!(use_cmd(None, None).is_replay_noop_after(&full));
let replay = boxcar::Vec::new();
record_replayable(&replay, full);
record_replayable(&replay, use_cmd(None, None));
assert_eq!(replay.count(), 1);
}
}