diff --git a/plugin/src/App/init.lua b/plugin/src/App/init.lua index 06a390e7..2e10ee60 100644 --- a/plugin/src/App/init.lua +++ b/plugin/src/App/init.lua @@ -1,5 +1,6 @@ local ChangeHistoryService = game:GetService("ChangeHistoryService") local Players = game:GetService("Players") +local SerializationService = game:GetService("SerializationService") local ServerStorage = game:GetService("ServerStorage") local RunService = game:GetService("RunService") @@ -640,18 +641,29 @@ function App:performSyncback() end local services = {} + local allChildren = {} for _, className in SYNCBACK_SERVICES do local ok, service = pcall(game.FindService, game, className) if ok and service then - table.insert(services, encodeService(service)) + local chunk = encodeService(service) + for _, child in service:GetChildren() do + table.insert(allChildren, child) + end + table.insert(services, chunk) end end + local data = buffer.create(0) + if #allChildren > 0 then + data = SerializationService:SerializeInstancesAsync(allChildren) + end + local url = ("http://%s:%s/api/syncback"):format(host, port) local body = Http.msgpackEncode({ protocolVersion = Config.protocolVersion, serverVersion = Config.expectedServerVersionString, placeId = game.PlaceId, + data = data, services = services, }) diff --git a/plugin/src/ChangeBatcher/encodeService.lua b/plugin/src/ChangeBatcher/encodeService.lua index 9cce3c7e..a726849b 100644 --- a/plugin/src/ChangeBatcher/encodeService.lua +++ b/plugin/src/ChangeBatcher/encodeService.lua @@ -1,5 +1,3 @@ -local SerializationService = game:GetService("SerializationService") - local Packages = script.Parent.Parent.Parent.Packages local Log = require(Packages.Log) local RbxDom = require(Packages.RbxDom) @@ -24,7 +22,8 @@ return function(service: Instance) continue end - local isReadable = propertyMeta.scriptability == "ReadWrite" or propertyMeta.scriptability == "Read" + local isReadable = propertyMeta.scriptability == "ReadWrite" + or propertyMeta.scriptability == "Read" local doesSerialize = propertyMeta.serialization ~= "DoesNotSerialize" if isReadable and doesSerialize then @@ -54,20 +53,9 @@ return function(service: Instance) end end - local children = service:GetChildren() - local data = buffer.create(0) - if #children > 0 then - local serializeOk, result = pcall(SerializationService.SerializeInstancesAsync, SerializationService, children) - if serializeOk then - data = result - else - Log.warn("Failed to serialize children of {}: {}", service.ClassName, tostring(result)) - end - end - local chunk = { className = service.ClassName, - data = data, + childCount = #service:GetChildren(), } if next(properties) then chunk.properties = properties diff --git a/src/cli/serve.rs b/src/cli/serve.rs index 4083dbd0..421b45ba 100644 --- a/src/cli/serve.rs +++ b/src/cli/serve.rs @@ -142,43 +142,47 @@ fn build_dom_from_chunks(payload: SyncbackPayload) -> anyhow::Result { let mut dom = WeakDom::new(InstanceBuilder::new("DataModel")); let root_ref = dom.root_ref(); - let mut global_ref_map: HashMap = HashMap::new(); let mut created_services: std::collections::HashSet = std::collections::HashSet::new(); + let cloned_children: Vec = if !payload.data.is_empty() { + let chunk_dom = rbx_binary::from_reader(Cursor::new(&payload.data)) + .context("Failed to parse rbxm data blob")?; + + let mut cloned = Vec::new(); + for &child_ref in chunk_dom.root().children() { + deep_clone_into(&chunk_dom, &mut dom, child_ref, root_ref, &mut global_ref_map); + let new_ref = *global_ref_map.get(&child_ref).unwrap(); + cloned.push(new_ref); + } + cloned + } else { + Vec::new() + }; + + let mut cursor = 0usize; for chunk in &payload.services { let mut builder = InstanceBuilder::new(&chunk.class_name); - for (key, value) in &chunk.properties { builder = builder.with_property(key.as_str(), value.clone()); } - let service_ref = dom.insert(root_ref, builder); created_services.insert(chunk.class_name.clone()); - if !chunk.data.is_empty() { - let chunk_dom = - rbx_binary::from_reader(Cursor::new(&chunk.data)).with_context(|| { - format!("Failed to parse rbxm for service {}", chunk.class_name) - })?; - - for &child_ref in chunk_dom.root().children() { - deep_clone_into( - &chunk_dom, - &mut dom, - child_ref, - service_ref, - &mut global_ref_map, - ); - } + let count = chunk.child_count as usize; + let end = (cursor + count).min(cloned_children.len()); + for &child_ref in &cloned_children[cursor..end] { + dom.transfer_within(child_ref, service_ref); } + cursor = end; if !chunk.refs.is_empty() { let children: Vec = dom.get_by_ref(service_ref).unwrap().children().to_vec(); for (prop_name, target) in &chunk.refs { let found = children.iter().find(|&&child_ref| { let child = dom.get_by_ref(child_ref).unwrap(); - child.name.as_str() == target.name && child.class.as_str() == target.class_name + child.name.as_str() == target.name + && child.class.as_str() == target.class_name }); if let Some(&child_ref) = found { let service = dom.get_by_ref_mut(service_ref).unwrap(); diff --git a/src/web/api.rs b/src/web/api.rs index c4792c62..0393240a 100644 --- a/src/web/api.rs +++ b/src/web/api.rs @@ -247,11 +247,13 @@ async fn handle_api_syncback( } log::info!( - "Live syncback requested with {} service chunks", - syncback_request.services.len() + "Live syncback requested with {} service chunks, {} bytes of rbxm data", + syncback_request.services.len(), + syncback_request.data.len() ); let payload = SyncbackPayload { + data: syncback_request.data, services: syncback_request.services, }; syncback_signal.fire(payload); diff --git a/src/web/interface.rs b/src/web/interface.rs index d6400218..a9da46f2 100644 --- a/src/web/interface.rs +++ b/src/web/interface.rs @@ -27,18 +27,21 @@ pub(crate) const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Current protocol version, which is required to match. pub const PROTOCOL_VERSION: u64 = 6; -/// A single service's children serialized as rbxm binary data, plus the -/// service's own properties and Ref property hints. +/// Metadata for a single service in the live syncback request. /// -/// Attributes and Tags are included in the `properties` map as -/// `Variant::Attributes(...)` and `Variant::Tags(...)` respectively, -/// matching the encoding used by the existing `/api/write` path. +/// Children are NOT stored here — they live in the top-level `data` blob +/// (a single rbxm containing all children from all services). `child_count` +/// tells the server how many consecutive children in that blob belong to +/// this service. +/// +/// Attributes and Tags are included in `properties` as +/// `Variant::Attributes(...)` / `Variant::Tags(...)`. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ServiceChunk { pub class_name: String, - #[serde(with = "serde_bytes")] - pub data: Vec, + #[serde(default)] + pub child_count: u32, #[serde(default)] pub properties: HashMap, #[serde(default)] @@ -64,12 +67,15 @@ pub struct SyncbackRequest { pub protocol_version: f64, pub server_version: String, pub place_id: Option, + #[serde(with = "serde_bytes", default)] + pub data: Vec, pub services: Vec, } /// Payload passed from the API handler to the serve loop after validation. #[derive(Debug)] pub struct SyncbackPayload { + pub data: Vec, pub services: Vec, } diff --git a/tests/rojo_test/serve_util.rs b/tests/rojo_test/serve_util.rs index eb3e785e..81a0b3a5 100644 --- a/tests/rojo_test/serve_util.rs +++ b/tests/rojo_test/serve_util.rs @@ -523,37 +523,30 @@ fn get_port_number() -> usize { port } -/// Build a ServiceChunk by serializing child instances into rbxm format. +/// A service entry plus its children (before serialization into the shared +/// rbxm blob). Use `build_syncback_request` to finalize. +pub struct ServiceEntry { + pub chunk: librojo::web_api::ServiceChunk, + pub children: Vec, +} + +/// Build a ServiceEntry with children and no extra properties. pub fn make_service_chunk( class_name: &str, children: Vec, -) -> librojo::web_api::ServiceChunk { +) -> ServiceEntry { make_service_chunk_full(class_name, vec![], vec![], children) } -/// Build a ServiceChunk with explicit service-level properties and ref hints -/// alongside the serialized children. Attributes and Tags should be included -/// in the `properties` vec as `Variant::Attributes(...)` / `Variant::Tags(...)`. +/// Build a ServiceEntry with explicit service-level properties and ref hints. pub fn make_service_chunk_full( class_name: &str, properties: Vec<(&str, rbx_dom_weak::types::Variant)>, refs: Vec<(&str, &str, &str)>, children: Vec, -) -> librojo::web_api::ServiceChunk { - use rbx_dom_weak::WeakDom; +) -> ServiceEntry { use std::collections::HashMap; - let mut dom = WeakDom::new(rbx_dom_weak::InstanceBuilder::new("DataModel")); - let root = dom.root_ref(); - for child in children { - dom.insert(root, child); - } - let child_refs: Vec = dom.root().children().to_vec(); - let mut buf = Vec::new(); - if !child_refs.is_empty() { - rbx_binary::to_writer(&mut buf, &dom, &child_refs).unwrap(); - } - let props_map: HashMap = properties .into_iter() .map(|(k, v)| (k.to_string(), v)) @@ -571,17 +564,50 @@ pub fn make_service_chunk_full( }) .collect(); - librojo::web_api::ServiceChunk { - class_name: class_name.to_string(), - data: buf, - properties: props_map, - refs: refs_map, + ServiceEntry { + chunk: librojo::web_api::ServiceChunk { + class_name: class_name.to_string(), + child_count: children.len() as u32, + properties: props_map, + refs: refs_map, + }, + children, } } -/// Build a complete rbxl file from service chunks, mirroring the same data -/// that live syncback receives. Used for CLI parity testing. -pub fn make_rbxl_from_chunks(chunks: &[librojo::web_api::ServiceChunk]) -> Vec { +/// Serialize all ServiceEntries into the final wire format: a single rbxm +/// data blob and a Vec of ServiceChunks (metadata only). +pub fn build_syncback_request( + entries: Vec, +) -> (Vec, Vec) { + use rbx_dom_weak::WeakDom; + + let mut dom = WeakDom::new(rbx_dom_weak::InstanceBuilder::new("DataModel")); + let root = dom.root_ref(); + + let mut chunks = Vec::new(); + for entry in entries { + for child in entry.children { + dom.insert(root, child); + } + chunks.push(entry.chunk); + } + + let all_refs: Vec = dom.root().children().to_vec(); + let mut data = Vec::new(); + if !all_refs.is_empty() { + rbx_binary::to_writer(&mut data, &dom, &all_refs).unwrap(); + } + + (data, chunks) +} + +/// Build a complete rbxl file from the single-rbxm data blob + service +/// chunks, mirroring the same data that live syncback receives. +pub fn make_rbxl_from_chunks( + data: &[u8], + chunks: &[librojo::web_api::ServiceChunk], +) -> Vec { use rbx_dom_weak::types::Variant; use rbx_dom_weak::WeakDom; use std::collections::HashMap; @@ -590,54 +616,66 @@ pub fn make_rbxl_from_chunks(chunks: &[librojo::web_api::ServiceChunk]) -> Vec = if !data.is_empty() { + let chunk_dom = rbx_binary::from_reader(Cursor::new(data)) + .unwrap_or_else(|e| panic!("Failed to parse rbxm data blob: {e}")); - for (key, value) in &chunk.properties { - builder = builder.with_property(key.as_str(), value.clone()); + let mut ref_map: HashMap = HashMap::new(); + let mut cloned = Vec::new(); + for &child_ref in chunk_dom.root().children() { + deep_clone_into_test(&chunk_dom, &mut dom, child_ref, root_ref, &mut ref_map); + cloned.push(*ref_map.get(&child_ref).unwrap()); } - let service_ref = dom.insert(root_ref, builder); - - if !chunk.data.is_empty() { - let chunk_dom = rbx_binary::from_reader(Cursor::new(&chunk.data)) - .unwrap_or_else(|e| panic!("Failed to parse rbxm for {}: {e}", chunk.class_name)); - - let mut ref_map: HashMap = HashMap::new(); - for &child_ref in chunk_dom.root().children() { - deep_clone_into_test(&chunk_dom, &mut dom, child_ref, service_ref, &mut ref_map); - } - - let all_refs: Vec = ref_map.values().copied().collect(); - for inst_ref in all_refs { - let props_to_fix: Vec<(String, Ref)> = { - let inst = dom.get_by_ref(inst_ref).unwrap(); - inst.properties - .iter() - .filter_map(|(key, value)| { - if let Variant::Ref(r) = value { - ref_map.get(r).map(|&mapped| (key.to_string(), mapped)) - } else { - None - } - }) - .collect() - }; - if !props_to_fix.is_empty() { - let inst = dom.get_by_ref_mut(inst_ref).unwrap(); - for (key, new_ref) in props_to_fix { - inst.properties.insert(key.into(), Variant::Ref(new_ref)); - } + let all_refs: Vec = ref_map.values().copied().collect(); + for inst_ref in all_refs { + let props_to_fix: Vec<(String, Ref)> = { + let inst = dom.get_by_ref(inst_ref).unwrap(); + inst.properties + .iter() + .filter_map(|(key, value)| { + if let Variant::Ref(r) = value { + ref_map.get(r).map(|&mapped| (key.to_string(), mapped)) + } else { + None + } + }) + .collect() + }; + if !props_to_fix.is_empty() { + let inst = dom.get_by_ref_mut(inst_ref).unwrap(); + for (key, new_ref) in props_to_fix { + inst.properties.insert(key.into(), Variant::Ref(new_ref)); } } } + cloned + } else { + Vec::new() + }; + + let mut cursor = 0usize; + for chunk in chunks { + let mut builder = rbx_dom_weak::InstanceBuilder::new(&chunk.class_name); + for (key, value) in &chunk.properties { + builder = builder.with_property(key.as_str(), value.clone()); + } + let service_ref = dom.insert(root_ref, builder); + + let count = chunk.child_count as usize; + let end = (cursor + count).min(cloned_children.len()); + for &child_ref in &cloned_children[cursor..end] { + dom.transfer_within(child_ref, service_ref); + } + cursor = end; if !chunk.refs.is_empty() { let children: Vec = dom.get_by_ref(service_ref).unwrap().children().to_vec(); for (prop_name, target) in &chunk.refs { let found = children.iter().find(|&&child_ref| { let child = dom.get_by_ref(child_ref).unwrap(); - child.name.as_str() == target.name && child.class.as_str() == target.class_name + child.name.as_str() == target.name + && child.class.as_str() == target.class_name }); if let Some(&child_ref) = found { let service = dom.get_by_ref_mut(service_ref).unwrap(); @@ -691,6 +729,7 @@ fn deep_clone_into_test( /// project path. pub fn run_cli_syncback_on_chunks( fixture_name: &str, + data: &[u8], chunks: &[librojo::web_api::ServiceChunk], ) -> (TempDir, PathBuf) { use crate::rojo_test::io_util::{copy_recursive, ROJO_PATH, SERVE_TESTS_PATH}; @@ -701,7 +740,7 @@ pub fn run_cli_syncback_on_chunks( fs::create_dir(&project_path).expect("Couldn't create project subdirectory"); copy_recursive(&source_path, &project_path).expect("Couldn't copy fixture"); - let rbxl_data = make_rbxl_from_chunks(chunks); + let rbxl_data = make_rbxl_from_chunks(data, chunks); let rbxl_path = dir.path().join("input.rbxl"); fs::write(&rbxl_path, rbxl_data).expect("Failed to write rbxl"); @@ -743,20 +782,34 @@ impl TestServeSession { } /// Post to /api/syncback with correct version/protocol info. + /// `data` is the single rbxm blob; `services` are the metadata chunks. /// Panics if the response is not 200. pub fn post_api_syncback( &self, place_id: Option, + data: Vec, services: Vec, ) { use serde::Serialize; - let request = serde_json::json!({ - "protocolVersion": librojo::web_api::PROTOCOL_VERSION, - "serverVersion": env!("CARGO_PKG_VERSION"), - "placeId": place_id, - "services": services, - }); + #[derive(Serialize)] + #[serde(rename_all = "camelCase")] + struct Req { + protocol_version: u64, + server_version: String, + place_id: Option, + #[serde(with = "serde_bytes")] + data: Vec, + services: Vec, + } + + let request = Req { + protocol_version: librojo::web_api::PROTOCOL_VERSION, + server_version: env!("CARGO_PKG_VERSION").to_string(), + place_id, + data, + services, + }; let mut body = Vec::new(); let mut serializer = rmp_serde::Serializer::new(&mut body) diff --git a/tests/tests/live_syncback.rs b/tests/tests/live_syncback.rs index 140dc2f9..035356d4 100644 --- a/tests/tests/live_syncback.rs +++ b/tests/tests/live_syncback.rs @@ -5,18 +5,24 @@ use rbx_dom_weak::InstanceBuilder; use crate::rojo_test::{ roundtrip_util::assert_dirs_equal, serve_util::{ - make_service_chunk, make_service_chunk_full, run_cli_syncback_on_chunks, run_serve_test, + build_syncback_request, make_service_chunk, make_service_chunk_full, + run_cli_syncback_on_chunks, run_serve_test, ServiceEntry, }, }; -use librojo::web_api::{ServiceChunk, SocketPacketType, PROTOCOL_VERSION}; +use librojo::web_api::{SocketPacketType, PROTOCOL_VERSION}; -fn assert_live_matches_cli(fixture_name: &str, chunks: &[ServiceChunk], place_id: Option) { +fn assert_live_matches_cli( + fixture_name: &str, + entries: Vec, + place_id: Option, +) { + let (data, chunks) = build_syncback_request(entries); run_serve_test(fixture_name, |session, _| { - session.post_api_syncback(place_id, chunks.to_vec()); + session.post_api_syncback(place_id, data.clone(), chunks.clone()); session.wait_to_come_back_online(); - let (_cli_dir, cli_path) = run_cli_syncback_on_chunks(fixture_name, chunks); + let (_cli_dir, cli_path) = run_cli_syncback_on_chunks(fixture_name, &data, &chunks); assert_dirs_equal(session.path(), &cli_path); }); } @@ -46,7 +52,7 @@ fn parity_scripts() { ), ], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -63,7 +69,7 @@ fn parity_models_with_properties() { )), )], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -76,7 +82,7 @@ fn parity_special_names() { InstanceBuilder::new("Folder").with_name("A/B"), ], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -89,7 +95,7 @@ fn parity_duplicate_names() { InstanceBuilder::new("Folder").with_name("Data"), ], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -111,7 +117,7 @@ fn parity_deep_hierarchy() { rbx_dom_weak::types::Variant::String("return true".into()), )])])])])], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -134,7 +140,7 @@ fn parity_mixed_file_types() { ), ], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -169,7 +175,7 @@ fn parity_multi_service() { .with_property("Anchored", rbx_dom_weak::types::Variant::Bool(true))], ), ]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -181,7 +187,7 @@ fn parity_empty_services_mixed() { ), make_service_chunk("ServerScriptService", vec![]), ]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } // ── Service property parity tests ──────────────────────────────── @@ -200,7 +206,7 @@ fn parity_lighting_properties() { vec![], vec![InstanceBuilder::new("PointLight").with_name("TestLight")], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -219,7 +225,7 @@ fn parity_soundservice_properties() { vec![], vec![], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -232,7 +238,7 @@ fn parity_starterplayer_properties() { vec![], vec![], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -248,7 +254,7 @@ fn parity_textchatservice_properties() { vec![], vec![], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -264,7 +270,7 @@ fn parity_childless_service_with_properties() { vec![], vec![], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -285,7 +291,7 @@ fn parity_service_properties_with_children() { .with_property("Source", Variant::String("return {}".into())), ], )]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); } #[test] @@ -310,13 +316,37 @@ fn parity_multiple_services_with_properties() { vec![InstanceBuilder::new("Folder").with_name("Assets")], ), ]; - assert_live_matches_cli("live_syncback", &chunks, None); + assert_live_matches_cli("live_syncback", chunks, None); +} + +#[test] +fn parity_cross_service_refs() { + use rbx_dom_weak::types::Variant; + + let target_part = InstanceBuilder::new("Part") + .with_name("TargetPart") + .with_property("Anchored", Variant::Bool(true)); + + let obj_value = InstanceBuilder::new("ObjectValue") + .with_name("Pointer") + .with_property("Value", Variant::Ref(target_part.referent())); + + let chunks = vec![ + make_service_chunk("Workspace", vec![target_part]), + make_service_chunk( + "ReplicatedStorage", + vec![InstanceBuilder::new("Folder") + .with_name("Refs") + .with_child(obj_value)], + ), + ]; + assert_live_matches_cli("live_syncback", chunks, None); } #[test] fn parity_camera_not_synced() { let camera_child = InstanceBuilder::new("Camera").with_name("Camera"); - let chunks = [make_service_chunk_full( + let entries = vec![make_service_chunk_full( "Workspace", vec![], vec![("CurrentCamera", "Camera", "Camera")], @@ -327,9 +357,10 @@ fn parity_camera_not_synced() { .with_property("Anchored", rbx_dom_weak::types::Variant::Bool(true)), ], )]; + let (data, chunks) = build_syncback_request(entries); run_serve_test("live_syncback", |session, _| { - session.post_api_syncback(None, chunks.to_vec()); + session.post_api_syncback(None, data.clone(), chunks.clone()); session.wait_to_come_back_online(); let workspace_dir = session.path().join("src/workspace"); @@ -485,12 +516,12 @@ fn rejects_unlisted_place() { #[test] fn allows_whitelisted_place() { - let chunks = vec![make_service_chunk( + let (data, chunks) = build_syncback_request(vec![make_service_chunk( "ReplicatedStorage", vec![InstanceBuilder::new("Folder").with_name("Allowed")], - )]; + )]); run_serve_test("live_syncback_place_ids", |session, _| { - session.post_api_syncback(Some(123), chunks); + session.post_api_syncback(Some(123), data, chunks); let new_info = session.wait_to_come_back_online(); assert!( !new_info.project_name.is_empty(), @@ -501,12 +532,12 @@ fn allows_whitelisted_place() { #[test] fn allows_any_place_when_unrestricted() { - let chunks = vec![make_service_chunk( + let (data, chunks) = build_syncback_request(vec![make_service_chunk( "ReplicatedStorage", vec![InstanceBuilder::new("Folder").with_name("Any")], - )]; + )]); run_serve_test("live_syncback", |session, _| { - session.post_api_syncback(Some(99999), chunks); + session.post_api_syncback(Some(99999), data, chunks); session.wait_to_come_back_online(); }); } @@ -515,7 +546,7 @@ fn allows_any_place_when_unrestricted() { #[test] fn server_comes_back_functional() { - let chunks = vec![make_service_chunk( + let (data, chunks) = build_syncback_request(vec![make_service_chunk( "ReplicatedStorage", vec![InstanceBuilder::new("ModuleScript") .with_name("TestModule") @@ -523,10 +554,10 @@ fn server_comes_back_functional() { "Source", rbx_dom_weak::types::Variant::String("return 42".into()), )], - )]; + )]); run_serve_test("live_syncback", |session, _| { - session.post_api_syncback(None, chunks); + session.post_api_syncback(None, data, chunks); let new_info = session.wait_to_come_back_online(); let read = session.get_api_read(new_info.root_instance_id).unwrap(); @@ -548,17 +579,17 @@ fn server_comes_back_functional() { #[test] fn syncback_twice_different_data() { - let chunks_a = vec![make_service_chunk( + let (data_a, chunks_a) = build_syncback_request(vec![make_service_chunk( "ReplicatedStorage", vec![InstanceBuilder::new("Folder").with_name("DataA")], - )]; - let chunks_b = vec![make_service_chunk( + )]); + let (data_b, chunks_b) = build_syncback_request(vec![make_service_chunk( "ReplicatedStorage", vec![InstanceBuilder::new("Folder").with_name("DataB")], - )]; + )]); run_serve_test("live_syncback", |session, _| { - session.post_api_syncback(None, chunks_a); + session.post_api_syncback(None, data_a, chunks_a); session.wait_to_come_back_online(); assert!( @@ -566,7 +597,7 @@ fn syncback_twice_different_data() { "First syncback should create DataA" ); - session.post_api_syncback(None, chunks_b); + session.post_api_syncback(None, data_b, chunks_b); session.wait_to_come_back_online(); assert!( @@ -582,7 +613,7 @@ fn syncback_twice_different_data() { #[test] fn syncback_replaces_all_old_files() { - let chunks = vec![make_service_chunk( + let (data, chunks) = build_syncback_request(vec![make_service_chunk( "ReplicatedStorage", vec![InstanceBuilder::new("ModuleScript") .with_name("NewFile") @@ -590,7 +621,7 @@ fn syncback_replaces_all_old_files() { "Source", rbx_dom_weak::types::Variant::String("return 'new'".into()), )], - )]; + )]); run_serve_test("live_syncback", |session, _| { assert!( @@ -598,7 +629,7 @@ fn syncback_replaces_all_old_files() { "OldModule should exist before syncback" ); - session.post_api_syncback(None, chunks); + session.post_api_syncback(None, data, chunks); session.wait_to_come_back_online(); assert!( @@ -624,16 +655,12 @@ fn roundtrip_build_syncback_rebuild() { let rbxl_data_a = fs::read(&rbxl_path_a).unwrap(); let dom_a = rbx_binary::from_reader(Cursor::new(&rbxl_data_a)).unwrap(); + let mut all_child_refs = Vec::new(); let mut chunks = Vec::new(); for &service_ref in dom_a.root().children() { let service = dom_a.get_by_ref(service_ref).unwrap(); let child_refs: Vec = service.children().to_vec(); - let mut data = Vec::new(); - if !child_refs.is_empty() { - rbx_binary::to_writer(&mut data, &dom_a, &child_refs).unwrap(); - } - let mut properties = std::collections::HashMap::new(); let mut refs_map = std::collections::HashMap::new(); for (key, value) in &service.properties { @@ -655,15 +682,21 @@ fn roundtrip_build_syncback_rebuild() { } } - chunks.push(ServiceChunk { + chunks.push(librojo::web_api::ServiceChunk { class_name: service.class.to_string(), - data, + child_count: child_refs.len() as u32, properties, refs: refs_map, }); + all_child_refs.extend(child_refs); } - session.post_api_syncback(None, chunks); + let mut data = Vec::new(); + if !all_child_refs.is_empty() { + rbx_binary::to_writer(&mut data, &dom_a, &all_child_refs).unwrap(); + } + + session.post_api_syncback(None, data, chunks); session.wait_to_come_back_online(); thread::sleep(Duration::from_millis(500));