use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;
pub const SUBPROTOCOL: &str = "leptoskit.live.v1.json";
pub const HEARTBEAT_SECONDS: u64 = 30;
pub const MAX_SUBSCRIPTIONS: usize = 256;
pub const MAX_RESULT_BYTES: usize = 512 * 1024;
pub const MAX_FRAME_BYTES: usize = 1024 * 1024;
pub const MAX_STREAMS: usize = 64;
pub const MAX_ACTIONS: usize = 16;
pub const MAX_CHANNELS: usize = 64;
pub const MAX_CHANNEL_KEY_BYTES: usize = 4 * 1024;
pub const MAX_PRESENCE_BYTES: usize = 16 * 1024;
pub const EVENTS_PATH: &str = "/live/events";
pub const FRAMES_PATH: &str = "/live/frames";
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientFrame {
Hello {
#[serde(default, skip_serializing_if = "Option::is_none")]
resume: Option<Resume>,
},
Auth {
token: String,
},
Sub {
id: u32,
query: String,
#[serde(default)]
args: Value,
},
Unsub {
id: u32,
},
Join {
channel: String,
name: String,
#[serde(default)]
args: Value,
#[serde(default)]
state: Value,
},
Leave {
channel: String,
},
Signal {
channel: String,
message: Value,
},
Mutate {
request: u32,
name: String,
#[serde(default)]
args: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
key: Option<String>,
},
Act {
request: u32,
name: String,
#[serde(default)]
args: Value,
},
Stream {
stream: u32,
name: String,
#[serde(default)]
args: Value,
#[serde(default)]
credits: u32,
},
Credit {
stream: u32,
n: u32,
},
Cancel {
stream: u32,
},
Ping,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Resume {
pub version: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub queries: Vec<ResumeQuery>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResumeQuery {
pub id: u32,
pub query: String,
#[serde(default)]
pub args: Value,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerFrame {
Welcome {
connection: String,
version: u64,
heartbeat_seconds: u64,
#[serde(default)]
outdated_client: bool,
},
Transition(Transition),
Done {
request: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
ok: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
err: Option<WireError>,
version: u64,
},
AuthStatus {
#[serde(default)]
refused: bool,
#[serde(default)]
expiring: bool,
},
Reconnect {
after_ms: u64,
},
Signal {
channel: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
from: Option<String>,
message: Value,
},
Presence {
channel: String,
members: Vec<PresenceMember>,
},
ChannelError {
channel: String,
err: WireError,
},
Item {
stream: u32,
value: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
cursor: Option<Value>,
},
End {
stream: u32,
reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
err: Option<WireError>,
},
Pong,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EndReason {
Done,
Failed(WireError),
Interrupted,
}
impl EndReason {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Done => "done",
Self::Failed(_) => "failed",
Self::Interrupted => "interrupted",
}
}
#[must_use]
pub fn frame(self, stream: u32) -> ServerFrame {
let reason = self.as_str().to_owned();
let err = match self {
Self::Failed(error) => Some(error),
Self::Done | Self::Interrupted => None,
};
ServerFrame::End {
stream,
reason,
err,
}
}
#[must_use]
pub fn from_frame(reason: &str, err: Option<WireError>) -> Self {
match (reason, err) {
("done", None) => Self::Done,
("interrupted", None) => Self::Interrupted,
(_, Some(error)) => Self::Failed(error),
(other, None) => {
Self::Failed(WireError {
status: 500,
message: format!("the stream ended: {other}"),
})
}
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Transition {
pub from: u64,
pub to: u64,
pub updates: Vec<SubscriptionUpdate>,
#[serde(default, skip_serializing_if = "is_false")]
pub full: bool,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SubscriptionUpdate {
pub id: u32,
#[serde(flatten)]
pub update: Update,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Update {
Ok(Value),
Patch(Patch),
Err(WireError),
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Patch {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub remove: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub upsert: Vec<Upsert>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub field: Option<String>,
#[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
pub set: serde_json::Map<String, Value>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Upsert {
pub at: usize,
pub value: Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PresenceMember {
pub id: String,
#[serde(default)]
pub state: Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WireError {
pub status: u16,
pub message: String,
}
#[cfg(feature = "live")]
impl WireError {
pub(crate) fn new(error: &crate::Error) -> Self {
Self {
status: error.status(),
message: error.message().to_owned(),
}
}
pub(crate) fn invalid(message: impl Into<String>) -> Self {
Self {
status: 400,
message: message.into(),
}
}
}
impl std::fmt::Display for WireError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for WireError {}
#[allow(
clippy::trivially_copy_pass_by_ref,
reason = "serde calls `skip_serializing_if` with a reference"
)]
const fn is_false(value: &bool) -> bool {
!*value
}
#[must_use]
pub fn patch(old: &Value, new: &Value) -> Update {
if old == new {
return Update::Ok(new.clone());
}
if let (Some(before), Some(after)) = (page_fields(old), page_fields(new)) {
return match diff_rows(&before["items"], &after["items"]) {
Some(mut patch) => {
patch.field = Some("items".to_owned());
patch.set = after
.iter()
.filter(|(name, value)| *name != "items" && before.get(*name) != Some(value))
.map(|(name, value)| (name.clone(), value.clone()))
.collect();
Update::Patch(patch)
}
None => Update::Ok(new.clone()),
};
}
match diff_rows(old, new) {
Some(patch) => Update::Patch(patch),
None => Update::Ok(new.clone()),
}
}
fn page_fields(value: &Value) -> Option<&serde_json::Map<String, Value>> {
let fields = value.as_object()?;
let shaped = fields.len() == 3
&& fields.get("items").is_some_and(Value::is_array)
&& fields.contains_key("end")
&& fields.get("has_more").is_some_and(Value::is_boolean);
shaped.then_some(fields)
}
fn diff_rows(old: &Value, new: &Value) -> Option<Patch> {
match (rows(old), rows(new)) {
(Some(before), Some(after)) => diff(&before, &after),
_ => None,
}
}
fn rows(value: &Value) -> Option<Vec<(String, Value)>> {
let items = value.as_array()?;
items
.iter()
.map(|item| {
let id = item.get("id")?.as_str()?;
Some((id.to_owned(), item.clone()))
})
.collect()
}
fn diff(before: &[(String, Value)], after: &[(String, Value)]) -> Option<Patch> {
let before_ids: std::collections::HashSet<&str> =
before.iter().map(|(id, _)| id.as_str()).collect();
if before_ids.len() != before.len() {
return None;
}
let after_ids: std::collections::HashSet<&str> =
after.iter().map(|(id, _)| id.as_str()).collect();
if after_ids.len() != after.len() {
return None;
}
let remove: Vec<String> = before
.iter()
.filter(|(id, _)| !after_ids.contains(id.as_str()))
.map(|(id, _)| id.clone())
.collect();
let stayed: Vec<&str> = before
.iter()
.map(|(id, _)| id.as_str())
.filter(|id| after_ids.contains(id))
.collect();
let after_stayed: Vec<&str> = after
.iter()
.map(|(id, _)| id.as_str())
.filter(|id| before_ids.contains(id))
.collect();
if stayed != after_stayed {
return None;
}
let previous: std::collections::HashMap<&str, &Value> =
before.iter().map(|(id, row)| (id.as_str(), row)).collect();
let upsert: Vec<Upsert> = after
.iter()
.enumerate()
.filter(|(_, (id, row))| previous.get(id.as_str()) != Some(&row))
.map(|(at, (_, row))| {
Upsert {
at,
value: row.clone(),
}
})
.collect();
Some(Patch {
remove,
upsert,
..Patch::default()
})
}
pub fn apply(value: &mut Value, update: &Update) {
match update {
Update::Ok(next) => *value = next.clone(),
Update::Err(_) => {}
Update::Patch(patch) => {
let list = match &patch.field {
Some(field) => {
let Some(fields) = value.as_object_mut() else {
return;
};
for (name, changed) in &patch.set {
fields.insert(name.clone(), changed.clone());
}
fields.get_mut(field)
}
None => Some(value),
};
let Some(items) = list.and_then(Value::as_array_mut) else {
return;
};
items.retain(|item| {
item.get("id")
.and_then(Value::as_str)
.is_none_or(|id| !patch.remove.iter().any(|removed| removed == id))
});
for upsert in &patch.upsert {
let id = upsert.value.get("id").and_then(Value::as_str);
let at = upsert.at.min(items.len());
match items.get_mut(at) {
Some(slot) if id.is_some() && slot.get("id").and_then(Value::as_str) == id => {
*slot = upsert.value.clone();
}
_ => items.insert(at, upsert.value.clone()),
}
}
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn frames_round_trip_through_json() {
let frames = [
ClientFrame::Hello {
resume: Some(Resume {
version: 7,
queries: vec![ResumeQuery {
id: 1,
query: "messages".into(),
args: json!({ "channel": "general" }),
}],
}),
},
ClientFrame::Act {
request: 3,
name: "summarize".into(),
args: json!({}),
},
ClientFrame::Auth {
token: "abc".into(),
},
ClientFrame::Sub {
id: 1,
query: "messages".into(),
args: json!({ "channel": "general" }),
},
ClientFrame::Unsub { id: 1 },
ClientFrame::Mutate {
request: 2,
name: "send_message".into(),
args: json!({}),
key: Some("k".into()),
},
ClientFrame::Stream {
stream: 5,
name: "tokens".into(),
args: json!({ "prompt": "hi" }),
credits: 8,
},
ClientFrame::Credit { stream: 5, n: 4 },
ClientFrame::Cancel { stream: 5 },
ClientFrame::Ping,
];
for frame in frames {
let json = serde_json::to_string(&frame).unwrap();
let back: ClientFrame = serde_json::from_str(&json).unwrap();
assert_eq!(back, frame);
}
let frames = [
ServerFrame::Welcome {
connection: "c1".into(),
version: 0,
heartbeat_seconds: 30,
outdated_client: false,
},
ServerFrame::Transition(Transition {
from: 0,
to: 1,
updates: vec![SubscriptionUpdate {
id: 1,
update: Update::Ok(json!([{ "id": "a" }])),
}],
full: false,
}),
ServerFrame::Done {
request: 2,
ok: Some(json!("ok")),
err: None,
version: 1,
},
ServerFrame::AuthStatus {
refused: false,
expiring: true,
},
ServerFrame::Reconnect { after_ms: 250 },
ServerFrame::Item {
stream: 5,
value: json!("tok"),
cursor: Some(json!(3)),
},
EndReason::Failed(WireError {
status: 500,
message: "gone".into(),
})
.frame(5),
EndReason::Done.frame(5),
ServerFrame::Pong,
];
for frame in frames {
let json = serde_json::to_string(&frame).unwrap();
let back: ServerFrame = serde_json::from_str(&json).unwrap();
assert_eq!(back, frame);
}
}
#[test]
fn an_older_stream_frame_still_parses() {
assert_eq!(
serde_json::from_str::<ServerFrame>(r#"{"type":"end","stream":2,"reason":"done"}"#)
.unwrap(),
EndReason::Done.frame(2)
);
assert_eq!(
serde_json::from_str::<ServerFrame>(r#"{"type":"item","stream":2,"value":1}"#).unwrap(),
ServerFrame::Item {
stream: 2,
value: json!(1),
cursor: None,
}
);
assert_eq!(EndReason::from_frame("done", None), EndReason::Done);
assert_eq!(
EndReason::from_frame("interrupted", None),
EndReason::Interrupted
);
assert!(matches!(
EndReason::from_frame("later", None),
EndReason::Failed(_)
));
}
#[test]
fn an_older_hello_still_parses() {
assert_eq!(
serde_json::from_str::<ClientFrame>(r#"{"type":"hello","resume":{"version":4}}"#)
.unwrap(),
ClientFrame::Hello {
resume: Some(Resume {
version: 4,
queries: Vec::new(),
}),
}
);
}
#[test]
fn unknown_frames_are_ignored() {
assert_eq!(
serde_json::from_str::<ClientFrame>(r#"{"type":"future"}"#).unwrap(),
ClientFrame::Unknown
);
assert_eq!(
serde_json::from_str::<ServerFrame>(r#"{"type":"future"}"#).unwrap(),
ServerFrame::Unknown
);
}
#[test]
fn lists_of_rows_change_by_patch() {
let before = json!([
{ "id": "a", "body": "one" },
{ "id": "b", "body": "two" },
{ "id": "c", "body": "three" }
]);
let after = json!([
{ "id": "a", "body": "one" },
{ "id": "b", "body": "TWO" },
{ "id": "d", "body": "four" }
]);
let Update::Patch(patch) = patch(&before, &after) else {
panic!("a keyed list patches");
};
assert_eq!(patch.remove, ["c"]);
assert_eq!(
patch.upsert,
[
Upsert {
at: 1,
value: json!({ "id": "b", "body": "TWO" })
},
Upsert {
at: 2,
value: json!({ "id": "d", "body": "four" })
}
]
);
let mut applied = before.clone();
apply(&mut applied, &Update::Patch(patch));
assert_eq!(applied, after);
}
#[test]
fn a_row_inserted_before_others_goes_in_rather_than_over() {
let before = json!([{ "id": "b" }, { "id": "d" }]);
let after = json!([{ "id": "a" }, { "id": "b" }, { "id": "c" }, { "id": "d", "n": 1 }]);
let update = patch(&before, &after);
assert!(matches!(update, Update::Patch(_)));
let mut applied = before.clone();
apply(&mut applied, &update);
assert_eq!(applied, after);
}
#[test]
fn a_page_changes_by_a_patch_of_its_items() {
let before = json!({
"items": [{ "id": "a", "n": 1 }, { "id": "b", "n": 2 }],
"end": "\"b\"",
"has_more": false
});
let after = json!({
"items": [{ "id": "a", "n": 1 }, { "id": "b", "n": 3 }, { "id": "c", "n": 4 }],
"end": "\"c\"",
"has_more": true
});
let Update::Patch(patch) = patch(&before, &after) else {
panic!("a page patches");
};
assert_eq!(patch.field.as_deref(), Some("items"));
assert_eq!(patch.upsert.len(), 2);
assert_eq!(patch.set.len(), 2, "end and has_more changed");
let mut applied = before.clone();
apply(&mut applied, &Update::Patch(patch));
assert_eq!(applied, after);
let other = json!({ "items": [{ "id": "a" }], "total": 1 });
let grown = json!({ "items": [{ "id": "a" }, { "id": "b" }], "total": 2 });
assert_eq!(patch_of(&other, &grown), Update::Ok(grown.clone()));
}
fn patch_of(old: &Value, new: &Value) -> Update {
patch(old, new)
}
#[test]
fn a_reorder_or_a_shape_without_ids_sends_the_whole_value() {
let before = json!([{ "id": "a" }, { "id": "b" }]);
let ast = json!([{ "id": "b" }, { "id": "a" }]);
assert_eq!(patch(&before, &ast), Update::Ok(ast.clone()));
let object = json!({ "id": "a" });
let other = json!({ "id": "b" });
assert_eq!(patch(&object, &other), Update::Ok(other.clone()));
let missing = json!([{ "body": "no id" }]);
assert_eq!(patch(&before, &missing), Update::Ok(missing.clone()));
}
#[test]
fn applying_an_ok_replaces_the_value_and_an_error_keeps_it() {
let mut value = json!([{ "id": "a" }]);
apply(&mut value, &Update::Ok(json!([])));
assert_eq!(value, json!([]));
let kept = value.clone();
apply(
&mut value,
&Update::Err(WireError {
status: 500,
message: "no connection".to_owned(),
}),
);
assert_eq!(value, kept);
}
}