Use msgpack for API

This commit is contained in:
Joe 2026-01-27 07:13:02 -06:00
parent 3d328c1d42
commit bda2af3dfb
12 changed files with 264 additions and 172 deletions

View file

@ -19,7 +19,6 @@ use rbx_dom_weak::{
};
use crate::{
json,
serve_session::ServeSession,
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
web::{
@ -28,12 +27,9 @@ use crate::{
ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
},
util::{json_ok, json_response},
},
web_api::{
BufferEncode, InstanceUpdate, RefPatchRequest, RefPatchResponse, SerializeRequest,
SerializeResponse,
util::{deserialize_msgpack, msgpack, msgpack_ok, serialize_msgpack},
},
web_api::{InstanceUpdate, RefPatchResponse, SerializeResponse},
};
pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>) -> Response<Body> {
@ -48,7 +44,7 @@ pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>)
if is_upgrade_request(&request) {
service.handle_api_socket(&mut request).await
} else {
json_response(
msgpack(
ErrorResponse::bad_request(
"/api/socket must be called as a websocket upgrade request",
),
@ -56,15 +52,19 @@ pub async fn call(serve_session: Arc<ServeSession>, mut request: Request<Body>)
)
}
}
(&Method::POST, "/api/serialize") => service.handle_api_serialize(request).await,
(&Method::POST, "/api/ref-patch") => service.handle_api_ref_patch(request).await,
(&Method::GET, path) if path.starts_with("/api/serialize/") => {
service.handle_api_serialize(request).await
}
(&Method::GET, path) if path.starts_with("/api/ref-patch/") => {
service.handle_api_ref_patch(request).await
}
(&Method::POST, path) if path.starts_with("/api/open/") => {
service.handle_api_open(request).await
}
(&Method::POST, "/api/write") => service.handle_api_write(request).await,
(_method, path) => json_response(
(_method, path) => msgpack(
ErrorResponse::not_found(format!("Route not found: {}", path)),
StatusCode::NOT_FOUND,
),
@ -85,7 +85,7 @@ impl ApiService {
let tree = self.serve_session.tree();
let root_instance_id = tree.get_root_id();
json_ok(&ServerInfoResponse {
msgpack_ok(&ServerInfoResponse {
server_version: SERVER_VERSION.to_owned(),
protocol_version: PROTOCOL_VERSION,
session_id: self.serve_session.session_id(),
@ -105,7 +105,7 @@ impl ApiService {
let input_cursor: u32 = match argument.parse() {
Ok(v) => v,
Err(err) => {
return json_response(
return msgpack(
ErrorResponse::bad_request(format!("Malformed message cursor: {}", err)),
StatusCode::BAD_REQUEST,
);
@ -116,7 +116,7 @@ impl ApiService {
let (response, websocket) = match upgrade(request, None) {
Ok(result) => result,
Err(err) => {
return json_response(
return msgpack(
ErrorResponse::internal_error(format!("WebSocket upgrade failed: {}", err)),
StatusCode::INTERNAL_SERVER_ERROR,
);
@ -143,10 +143,10 @@ impl ApiService {
let body = body::to_bytes(request.into_body()).await.unwrap();
let request: WriteRequest = match json::from_slice(&body) {
let request: WriteRequest = match deserialize_msgpack(&body) {
Ok(request) => request,
Err(err) => {
return json_response(
return msgpack(
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
StatusCode::BAD_REQUEST,
);
@ -154,7 +154,7 @@ impl ApiService {
};
if request.session_id != session_id {
return json_response(
return msgpack(
ErrorResponse::bad_request("Wrong session ID"),
StatusCode::BAD_REQUEST,
);
@ -180,7 +180,7 @@ impl ApiService {
})
.unwrap();
json_ok(WriteResponse { session_id })
msgpack_ok(WriteResponse { session_id })
}
async fn handle_api_read(&self, request: Request<Body>) -> Response<Body> {
@ -190,7 +190,7 @@ impl ApiService {
let requested_ids = match requested_ids {
Ok(ids) => ids,
Err(_) => {
return json_response(
return msgpack(
ErrorResponse::bad_request("Malformed ID list"),
StatusCode::BAD_REQUEST,
);
@ -254,7 +254,7 @@ impl ApiService {
}
}
json_ok(ReadResponse {
msgpack_ok(ReadResponse {
session_id: self.serve_session.session_id(),
message_cursor,
instances,
@ -269,30 +269,23 @@ impl ApiService {
/// that correspond to the requested Instances. These values have their
/// `Value` property set to point to the requested Instance.
async fn handle_api_serialize(&self, request: Request<Body>) -> Response<Body> {
let session_id = self.serve_session.session_id();
let body = body::to_bytes(request.into_body()).await.unwrap();
let argument = &request.uri().path()["/api/serialize/".len()..];
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
let request: SerializeRequest = match json::from_slice(&body) {
Ok(request) => request,
Err(err) => {
return json_response(
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
let requested_ids = match requested_ids {
Ok(ids) => ids,
Err(_) => {
return msgpack(
ErrorResponse::bad_request("Malformed ID list"),
StatusCode::BAD_REQUEST,
);
}
};
if request.session_id != session_id {
return json_response(
ErrorResponse::bad_request("Wrong session ID"),
StatusCode::BAD_REQUEST,
);
}
let mut response_dom = WeakDom::new(InstanceBuilder::new("Folder"));
let tree = self.serve_session.tree();
for id in &request.ids {
for id in &requested_ids {
if let Some(instance) = tree.get_instance(*id) {
let clone = response_dom.insert(
Ref::none(),
@ -316,7 +309,7 @@ impl ApiService {
response_dom.transfer_within(child_ref, object_value);
} else {
json_response(
msgpack(
ErrorResponse::bad_request(format!("provided id {id} is not in the tree")),
StatusCode::BAD_REQUEST,
);
@ -327,9 +320,9 @@ impl ApiService {
let mut source = Vec::new();
rbx_binary::to_writer(&mut source, &response_dom, &[response_dom.root_ref()]).unwrap();
json_ok(SerializeResponse {
msgpack_ok(SerializeResponse {
session_id: self.serve_session.session_id(),
model_contents: BufferEncode::new(source),
model_contents: source,
})
}
@ -337,26 +330,22 @@ impl ApiService {
/// provided IDs. Used because the plugin does not store a RojoTree,
/// and referent properties need to be updated after the serialize
/// endpoint is used.
async fn handle_api_ref_patch(self, request: Request<Body>) -> Response<Body> {
let session_id = self.serve_session.session_id();
let body = body::to_bytes(request.into_body()).await.unwrap();
async fn handle_api_ref_patch(&self, request: Request<Body>) -> Response<Body> {
let argument = &request.uri().path()["/api/ref-patch/".len()..];
let requested_ids: Result<Vec<Ref>, _> = argument.split(',').map(Ref::from_str).collect();
let request: RefPatchRequest = match json::from_slice(&body) {
Ok(request) => request,
Err(err) => {
return json_response(
ErrorResponse::bad_request(format!("Invalid body: {}", err)),
let requested_ids = match requested_ids {
Ok(ids) => ids,
Err(_) => {
return msgpack(
ErrorResponse::bad_request("Malformed ID list"),
StatusCode::BAD_REQUEST,
);
}
};
if request.session_id != session_id {
return json_response(
ErrorResponse::bad_request("Wrong session ID"),
StatusCode::BAD_REQUEST,
);
}
// Convert to HashSet for efficient lookup
let ids: HashSet<Ref> = requested_ids.into_iter().collect();
let mut instance_updates: HashMap<Ref, InstanceUpdate> = HashMap::new();
@ -366,7 +355,7 @@ impl ApiService {
let Variant::Ref(prop_value) = prop_value else {
continue;
};
if let Some(target_id) = request.ids.get(prop_value) {
if let Some(target_id) = ids.get(prop_value) {
let instance_id = instance.id();
let update =
instance_updates
@ -385,7 +374,7 @@ impl ApiService {
}
}
json_ok(RefPatchResponse {
msgpack_ok(RefPatchResponse {
session_id: self.serve_session.session_id(),
patch: SubscribeMessage {
added: HashMap::new(),
@ -401,7 +390,7 @@ impl ApiService {
let requested_id = match Ref::from_str(argument) {
Ok(id) => id,
Err(_) => {
return json_response(
return msgpack(
ErrorResponse::bad_request("Invalid instance ID"),
StatusCode::BAD_REQUEST,
);
@ -413,7 +402,7 @@ impl ApiService {
let instance = match tree.get_instance(requested_id) {
Some(instance) => instance,
None => {
return json_response(
return msgpack(
ErrorResponse::bad_request("Instance not found"),
StatusCode::NOT_FOUND,
);
@ -423,7 +412,7 @@ impl ApiService {
let script_path = match pick_script_path(instance) {
Some(path) => path,
None => {
return json_response(
return msgpack(
ErrorResponse::bad_request(
"No appropriate file could be found to open this script",
),
@ -436,7 +425,7 @@ impl ApiService {
Ok(()) => {}
Err(error) => match error {
OpenError::Io(io_error) => {
return json_response(
return msgpack(
ErrorResponse::internal_error(format!(
"Attempting to open {} failed because of the following io error: {}",
script_path.display(),
@ -450,7 +439,7 @@ impl ApiService {
status,
stderr,
} => {
return json_response(
return msgpack(
ErrorResponse::internal_error(format!(
r#"The command '{}' to open '{}' failed with the error code '{}'.
Error logs:
@ -466,7 +455,7 @@ impl ApiService {
},
};
json_ok(OpenResponse {
msgpack_ok(OpenResponse {
session_id: self.serve_session.session_id(),
})
}
@ -531,7 +520,7 @@ async fn handle_websocket_subscription(
match result {
Ok((new_cursor, messages)) => {
if !messages.is_empty() {
let json_message = {
let msgpack_message = {
let tree = tree_handle.lock().unwrap();
let api_messages: Vec<_> = messages
.into_iter()
@ -555,13 +544,12 @@ async fn handle_websocket_subscription(
}),
};
// Use compact JSON5 encoder for transport efficiency, preserves NaN/Infinity
json::to_string_compact(&response)?
serialize_msgpack(response)?
};
log::debug!("Sending batch of messages over WebSocket subscription");
if websocket.send(Message::Text(json_message)).await.is_err() {
if websocket.send(Message::Binary(msgpack_message)).await.is_err() {
// Client disconnected
log::debug!("WebSocket subscription closed by client");
break;
@ -677,7 +665,7 @@ fn filter_subscribe_message_for_scripts(
// ALL instances get ignoreUnknownInstances: true in scripts-only mode
// This ensures we only sync script content, never delete/modify other instances
// Non-scripts also have their properties cleared - they're just tree structure
for (_id, inst) in &mut msg.added {
for inst in msg.added.values_mut() {
inst.metadata = Some(InstanceMetadata {
ignore_unknown_instances: true,
});