mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Update dependencies and improve syncback handling
- Updated versions of several dependencies in `Cargo.lock`, including `async-compression` (0.4.40), `aws-lc-rs` (1.16.0), `bumpalo` (3.20.1), and `compression-codecs` (0.4.37), ensuring compatibility and access to the latest features and fixes. - Refactored the `fire` method in `SyncbackSignal` to return a boolean indicating whether a syncback was successfully initiated, preventing concurrent syncbacks. - Enhanced error handling in the API syncback process to return a conflict response if a syncback is already in progress. - Improved the `serve.rs` file by replacing `forget` with `drop` for better memory management and added checks to ensure service chunk integrity during live syncback operations. - Added new tests for edge cases and resilience in live syncback functionality, ensuring robustness against malformed data and rapid sequential requests. These changes aim to enhance the reliability and performance of the syncback feature while keeping dependencies up to date.
This commit is contained in:
parent
69f0b9fd63
commit
5b38648ead
12 changed files with 387 additions and 117 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -160,9 +160,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.39"
|
version = "0.4.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68650b7df54f0293fd061972a0fb05aaf4fc0879d3b3d21a638a182c5c543b9f"
|
checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"compression-codecs",
|
"compression-codecs",
|
||||||
"compression-core",
|
"compression-core",
|
||||||
|
|
@ -265,9 +265,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.15.4"
|
version = "1.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256"
|
checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
|
|
@ -418,9 +418,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.19.1"
|
version = "3.20.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
checksum = "5c6f81257d10a0f602a294ae4182251151ff97dbb504ef9afcdda4a64b24d9b4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "byteorder"
|
name = "byteorder"
|
||||||
|
|
@ -602,9 +602,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compression-codecs"
|
name = "compression-codecs"
|
||||||
version = "0.4.36"
|
version = "0.4.37"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a"
|
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"compression-core",
|
"compression-core",
|
||||||
|
|
|
||||||
|
|
@ -621,7 +621,14 @@ matchChildren = function(
|
||||||
if precomputed ~= nil then
|
if precomputed ~= nil then
|
||||||
totalCost += precomputed
|
totalCost += precomputed
|
||||||
else
|
else
|
||||||
totalCost += computePairCost(session, pair.virtualId, pair.studioInstance, virtualInstances, math.huge, depth)
|
totalCost += computePairCost(
|
||||||
|
session,
|
||||||
|
pair.virtualId,
|
||||||
|
pair.studioInstance,
|
||||||
|
virtualInstances,
|
||||||
|
math.huge,
|
||||||
|
depth
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
totalCost += (#unmatchedVirtual + #unmatchedStudio) * UNMATCHED_PENALTY
|
totalCost += (#unmatchedVirtual + #unmatchedStudio) * UNMATCHED_PENALTY
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,13 @@ local function trueEquals(a: any, b: any): boolean
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
for i = 1, #aKp do
|
for i = 1, #aKp do
|
||||||
if not (fuzzyEq(aKp[i].Time, bKp[i].Time) and fuzzyEq(aKp[i].Value, bKp[i].Value) and fuzzyEq(aKp[i].Envelope, bKp[i].Envelope)) then
|
if
|
||||||
|
not (
|
||||||
|
fuzzyEq(aKp[i].Time, bKp[i].Time)
|
||||||
|
and fuzzyEq(aKp[i].Value, bKp[i].Value)
|
||||||
|
and fuzzyEq(aKp[i].Envelope, bKp[i].Envelope)
|
||||||
|
)
|
||||||
|
then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -452,40 +452,54 @@ return function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("from Vector2 identical", function()
|
it("from Vector2 identical", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
Rect.new(Vector2.new(10, 20), Vector2.new(30, 40)),
|
trueEquals(
|
||||||
Rect.new(Vector2.new(10, 20), Vector2.new(30, 40))
|
Rect.new(Vector2.new(10, 20), Vector2.new(30, 40)),
|
||||||
)).to.equal(true)
|
Rect.new(Vector2.new(10, 20), Vector2.new(30, 40))
|
||||||
|
)
|
||||||
|
).to.equal(true)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("NumberSequence", function()
|
describe("NumberSequence", function()
|
||||||
it("identical two-keypoint", function()
|
it("identical two-keypoint", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
|
trueEquals(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) })
|
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
|
||||||
)).to.equal(true)
|
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) })
|
||||||
|
)
|
||||||
|
).to.equal(true)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("value within epsilon", function()
|
it("value within epsilon", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
|
trueEquals(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1.00005) })
|
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
|
||||||
)).to.equal(true)
|
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1.00005) })
|
||||||
|
)
|
||||||
|
).to.equal(true)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("value beyond epsilon", function()
|
it("value beyond epsilon", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
|
trueEquals(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1.002) })
|
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
|
||||||
)).to.equal(false)
|
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1.002) })
|
||||||
|
)
|
||||||
|
).to.equal(false)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("different keypoint count", function()
|
it("different keypoint count", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
|
trueEquals(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(0.5, 0.5), NumberSequenceKeypoint.new(1, 1) })
|
NumberSequence.new({ NumberSequenceKeypoint.new(0, 0), NumberSequenceKeypoint.new(1, 1) }),
|
||||||
)).to.equal(false)
|
NumberSequence.new({
|
||||||
|
NumberSequenceKeypoint.new(0, 0),
|
||||||
|
NumberSequenceKeypoint.new(0.5, 0.5),
|
||||||
|
NumberSequenceKeypoint.new(1, 1),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).to.equal(false)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("simple constructor identical", function()
|
it("simple constructor identical", function()
|
||||||
|
|
@ -493,51 +507,99 @@ return function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("envelope within epsilon", function()
|
it("envelope within epsilon", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 1, 0.5), NumberSequenceKeypoint.new(1, 1, 0.5) }),
|
trueEquals(
|
||||||
NumberSequence.new({ NumberSequenceKeypoint.new(0, 1, 0.50005), NumberSequenceKeypoint.new(1, 1, 0.5) })
|
NumberSequence.new({ NumberSequenceKeypoint.new(0, 1, 0.5), NumberSequenceKeypoint.new(1, 1, 0.5) }),
|
||||||
)).to.equal(true)
|
NumberSequence.new({
|
||||||
|
NumberSequenceKeypoint.new(0, 1, 0.50005),
|
||||||
|
NumberSequenceKeypoint.new(1, 1, 0.5),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).to.equal(true)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe("ColorSequence", function()
|
describe("ColorSequence", function()
|
||||||
it("identical two-keypoint", function()
|
it("identical two-keypoint", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
|
trueEquals(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
|
ColorSequence.new({
|
||||||
)).to.equal(true)
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
}),
|
||||||
|
ColorSequence.new({
|
||||||
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).to.equal(true)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("color within epsilon", function()
|
it("color within epsilon", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
|
trueEquals(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(0.50005, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
|
ColorSequence.new({
|
||||||
)).to.equal(true)
|
ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
}),
|
||||||
|
ColorSequence.new({
|
||||||
|
ColorSequenceKeypoint.new(0, Color3.new(0.50005, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).to.equal(true)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("color beyond epsilon", function()
|
it("color beyond epsilon", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
|
trueEquals(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(0.6, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
|
ColorSequence.new({
|
||||||
)).to.equal(false)
|
ColorSequenceKeypoint.new(0, Color3.new(0.5, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
}),
|
||||||
|
ColorSequence.new({
|
||||||
|
ColorSequenceKeypoint.new(0, Color3.new(0.6, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).to.equal(false)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("different keypoint count", function()
|
it("different keypoint count", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
|
trueEquals(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(0.5, Color3.new(0, 1, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
|
ColorSequence.new({
|
||||||
)).to.equal(false)
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
}),
|
||||||
|
ColorSequence.new({
|
||||||
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(0.5, Color3.new(0, 1, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).to.equal(false)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("simple constructor identical", function()
|
it("simple constructor identical", function()
|
||||||
expect(trueEquals(ColorSequence.new(Color3.new(1, 0, 0)), ColorSequence.new(Color3.new(1, 0, 0)))).to.equal(true)
|
expect(trueEquals(ColorSequence.new(Color3.new(1, 0, 0)), ColorSequence.new(Color3.new(1, 0, 0)))).to.equal(
|
||||||
|
true
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it("time within epsilon", function()
|
it("time within epsilon", function()
|
||||||
expect(trueEquals(
|
expect(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) }),
|
trueEquals(
|
||||||
ColorSequence.new({ ColorSequenceKeypoint.new(0.00005, Color3.new(1, 0, 0)), ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)) })
|
ColorSequence.new({
|
||||||
)).to.equal(true)
|
ColorSequenceKeypoint.new(0, Color3.new(1, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
}),
|
||||||
|
ColorSequence.new({
|
||||||
|
ColorSequenceKeypoint.new(0.00005, Color3.new(1, 0, 0)),
|
||||||
|
ColorSequenceKeypoint.new(1, Color3.new(0, 0, 1)),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).to.equal(true)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
|
||||||
2
rbx-dom
2
rbx-dom
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1166e5a628fa7cba9c85cd660ae8958bb6e9f458
|
Subproject commit a98c70c8752071b59b852d2f7e55376b1bbd2370
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::Cursor,
|
io::Cursor,
|
||||||
mem::forget,
|
|
||||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
|
@ -58,7 +57,7 @@ impl ServeCommand {
|
||||||
.port
|
.port
|
||||||
.or_else(|| session.project_port())
|
.or_else(|| session.project_port())
|
||||||
.unwrap_or(DEFAULT_PORT);
|
.unwrap_or(DEFAULT_PORT);
|
||||||
forget(session);
|
drop(session);
|
||||||
(ip, port)
|
(ip, port)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -83,8 +82,12 @@ impl ServeCommand {
|
||||||
match server.start(addr) {
|
match server.start(addr) {
|
||||||
ServerExitReason::SyncbackRequested(payload) => {
|
ServerExitReason::SyncbackRequested(payload) => {
|
||||||
log::info!("Live syncback requested, running...");
|
log::info!("Live syncback requested, running...");
|
||||||
run_live_syncback(&project_path, payload)?;
|
match run_live_syncback(&project_path, payload) {
|
||||||
log::info!("Syncback complete, restarting serve...");
|
Ok(()) => log::info!("Syncback complete, restarting serve..."),
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Live syncback failed: {err:#}. Restarting serve...")
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +135,7 @@ fn run_live_syncback(project_path: &Path, payload: SyncbackPayload) -> anyhow::R
|
||||||
|
|
||||||
crate::git::refresh_git_index(base_path);
|
crate::git::refresh_git_index(base_path);
|
||||||
|
|
||||||
forget(session_old);
|
drop(session_old);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +187,15 @@ fn build_dom_from_chunks(payload: SyncbackPayload) -> anyhow::Result<WeakDom> {
|
||||||
let child_count = chunk.child_count as usize;
|
let child_count = chunk.child_count as usize;
|
||||||
let ref_count = chunk.ref_target_count as usize;
|
let ref_count = chunk.ref_target_count as usize;
|
||||||
let total = child_count + ref_count;
|
let total = child_count + ref_count;
|
||||||
let end = (cursor + total).min(cloned_children.len());
|
let end = cursor + total;
|
||||||
|
anyhow::ensure!(
|
||||||
|
end <= cloned_children.len(),
|
||||||
|
"Service '{}' claims {} children + {} carriers but only {} instances remain in blob",
|
||||||
|
chunk.class_name,
|
||||||
|
child_count,
|
||||||
|
ref_count,
|
||||||
|
cloned_children.len().saturating_sub(cursor)
|
||||||
|
);
|
||||||
let service_range: Vec<Ref> = cloned_children[cursor..end].to_vec();
|
let service_range: Vec<Ref> = cloned_children[cursor..end].to_vec();
|
||||||
|
|
||||||
for &child_ref in &service_range[..child_count.min(service_range.len())] {
|
for &child_ref in &service_range[..child_count.min(service_range.len())] {
|
||||||
|
|
@ -210,6 +221,13 @@ fn build_dom_from_chunks(payload: SyncbackPayload) -> anyhow::Result<WeakDom> {
|
||||||
cursor = end;
|
cursor = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
anyhow::ensure!(
|
||||||
|
cursor == cloned_children.len(),
|
||||||
|
"Service chunks account for {} instances but rbxm blob contains {}",
|
||||||
|
cursor,
|
||||||
|
cloned_children.len()
|
||||||
|
);
|
||||||
|
|
||||||
for &service_name in VISIBLE_SERVICES {
|
for &service_name in VISIBLE_SERVICES {
|
||||||
if !created_services.contains(service_name) {
|
if !created_services.contains(service_name) {
|
||||||
dom.insert(root_ref, InstanceBuilder::new(service_name));
|
dom.insert(root_ref, InstanceBuilder::new(service_name));
|
||||||
|
|
|
||||||
|
|
@ -1528,16 +1528,10 @@ mod tests {
|
||||||
.insert(ustr("Transparency"), Variant::Float32(0.0));
|
.insert(ustr("Transparency"), Variant::Float32(0.0));
|
||||||
s
|
s
|
||||||
};
|
};
|
||||||
let tree_model_grip_snap = make_snapshot_with_children(
|
let tree_model_grip_snap =
|
||||||
"Weapon",
|
make_snapshot_with_children("Weapon", "Model", vec![tree_child_grip]);
|
||||||
"Model",
|
let tree_model_handle_snap =
|
||||||
vec![tree_child_grip],
|
make_snapshot_with_children("Weapon", "Model", vec![tree_child_handle]);
|
||||||
);
|
|
||||||
let tree_model_handle_snap = make_snapshot_with_children(
|
|
||||||
"Weapon",
|
|
||||||
"Model",
|
|
||||||
vec![tree_child_handle],
|
|
||||||
);
|
|
||||||
|
|
||||||
let (tree2, tree_refs2) =
|
let (tree2, tree_refs2) =
|
||||||
make_tree_from_snapshots(vec![tree_model_grip_snap, tree_model_handle_snap]);
|
make_tree_from_snapshots(vec![tree_model_grip_snap, tree_model_handle_snap]);
|
||||||
|
|
@ -1559,11 +1553,8 @@ mod tests {
|
||||||
s
|
s
|
||||||
};
|
};
|
||||||
let snap_grip2 = {
|
let snap_grip2 = {
|
||||||
let mut s = make_snapshot_with_children(
|
let mut s =
|
||||||
"Weapon",
|
make_snapshot_with_children("Weapon", "Model", vec![make_snapshot("Grip", "Part")]);
|
||||||
"Model",
|
|
||||||
vec![make_snapshot("Grip", "Part")],
|
|
||||||
);
|
|
||||||
s.properties
|
s.properties
|
||||||
.insert(ustr("PrimaryPart"), Variant::Ref(tree2_grip_ref));
|
.insert(ustr("PrimaryPart"), Variant::Ref(tree2_grip_ref));
|
||||||
s
|
s
|
||||||
|
|
|
||||||
|
|
@ -880,10 +880,8 @@ mod tests {
|
||||||
let mut new_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
let mut new_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||||
let new_root = new_dom.root_ref();
|
let new_root = new_dom.root_ref();
|
||||||
|
|
||||||
let new_model_a = new_dom.insert(
|
let new_model_a =
|
||||||
new_root,
|
new_dom.insert(new_root, InstanceBuilder::new("Model").with_name("Weapon"));
|
||||||
InstanceBuilder::new("Model").with_name("Weapon"),
|
|
||||||
);
|
|
||||||
let new_handle = new_dom.insert(
|
let new_handle = new_dom.insert(
|
||||||
new_model_a,
|
new_model_a,
|
||||||
InstanceBuilder::new("Part").with_name("Handle"),
|
InstanceBuilder::new("Part").with_name("Handle"),
|
||||||
|
|
@ -894,14 +892,9 @@ mod tests {
|
||||||
.properties
|
.properties
|
||||||
.insert("PrimaryPart".into(), Variant::Ref(new_handle));
|
.insert("PrimaryPart".into(), Variant::Ref(new_handle));
|
||||||
|
|
||||||
let new_model_b = new_dom.insert(
|
let new_model_b =
|
||||||
new_root,
|
new_dom.insert(new_root, InstanceBuilder::new("Model").with_name("Weapon"));
|
||||||
InstanceBuilder::new("Model").with_name("Weapon"),
|
let new_grip = new_dom.insert(new_model_b, InstanceBuilder::new("Part").with_name("Grip"));
|
||||||
);
|
|
||||||
let new_grip = new_dom.insert(
|
|
||||||
new_model_b,
|
|
||||||
InstanceBuilder::new("Part").with_name("Grip"),
|
|
||||||
);
|
|
||||||
new_dom
|
new_dom
|
||||||
.get_by_ref_mut(new_model_b)
|
.get_by_ref_mut(new_model_b)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|
@ -912,24 +905,17 @@ mod tests {
|
||||||
let mut old_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
let mut old_dom = WeakDom::new(InstanceBuilder::new("DataModel"));
|
||||||
let old_root = old_dom.root_ref();
|
let old_root = old_dom.root_ref();
|
||||||
|
|
||||||
let old_model_b = old_dom.insert(
|
let old_model_b =
|
||||||
old_root,
|
old_dom.insert(old_root, InstanceBuilder::new("Model").with_name("Weapon"));
|
||||||
InstanceBuilder::new("Model").with_name("Weapon"),
|
let old_grip = old_dom.insert(old_model_b, InstanceBuilder::new("Part").with_name("Grip"));
|
||||||
);
|
|
||||||
let old_grip = old_dom.insert(
|
|
||||||
old_model_b,
|
|
||||||
InstanceBuilder::new("Part").with_name("Grip"),
|
|
||||||
);
|
|
||||||
old_dom
|
old_dom
|
||||||
.get_by_ref_mut(old_model_b)
|
.get_by_ref_mut(old_model_b)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.properties
|
.properties
|
||||||
.insert("PrimaryPart".into(), Variant::Ref(old_grip));
|
.insert("PrimaryPart".into(), Variant::Ref(old_grip));
|
||||||
|
|
||||||
let old_model_a = old_dom.insert(
|
let old_model_a =
|
||||||
old_root,
|
old_dom.insert(old_root, InstanceBuilder::new("Model").with_name("Weapon"));
|
||||||
InstanceBuilder::new("Model").with_name("Weapon"),
|
|
||||||
);
|
|
||||||
let old_handle = old_dom.insert(
|
let old_handle = old_dom.insert(
|
||||||
old_model_a,
|
old_model_a,
|
||||||
InstanceBuilder::new("Part").with_name("Handle"),
|
InstanceBuilder::new("Part").with_name("Handle"),
|
||||||
|
|
@ -961,14 +947,8 @@ mod tests {
|
||||||
let new_inst = new_dom.get_by_ref(*new_ref).unwrap();
|
let new_inst = new_dom.get_by_ref(*new_ref).unwrap();
|
||||||
let old_inst = old_dom.get_by_ref(*old_ref).unwrap();
|
let old_inst = old_dom.get_by_ref(*old_ref).unwrap();
|
||||||
|
|
||||||
let new_child_name = &new_dom
|
let new_child_name = &new_dom.get_by_ref(new_inst.children()[0]).unwrap().name;
|
||||||
.get_by_ref(new_inst.children()[0])
|
let old_child_name = &old_dom.get_by_ref(old_inst.children()[0]).unwrap().name;
|
||||||
.unwrap()
|
|
||||||
.name;
|
|
||||||
let old_child_name = &old_dom
|
|
||||||
.get_by_ref(old_inst.children()[0])
|
|
||||||
.unwrap()
|
|
||||||
.name;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
new_child_name, old_child_name,
|
new_child_name, old_child_name,
|
||||||
"Ref scoring failed: new model with child '{}' matched old model with child '{}'",
|
"Ref scoring failed: new model with child '{}' matched old model with child '{}'",
|
||||||
|
|
|
||||||
|
|
@ -256,7 +256,12 @@ async fn handle_api_syncback(
|
||||||
data: syncback_request.data,
|
data: syncback_request.data,
|
||||||
services: syncback_request.services,
|
services: syncback_request.services,
|
||||||
};
|
};
|
||||||
syncback_signal.fire(payload);
|
if !syncback_signal.fire(payload) {
|
||||||
|
return msgpack(
|
||||||
|
ErrorResponse::bad_request("A syncback is already in progress"),
|
||||||
|
StatusCode::CONFLICT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
msgpack_ok(serde_json::json!({"status": "syncback_initiated"}))
|
msgpack_ok(serde_json::json!({"status": "syncback_initiated"}))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ pub const PROTOCOL_VERSION: u64 = 6;
|
||||||
|
|
||||||
/// Metadata for a single service in the live syncback request.
|
/// Metadata for a single service in the live syncback request.
|
||||||
///
|
///
|
||||||
/// Children are NOT stored here — they live in the top-level `data` blob
|
/// Children are NOT stored here -- they live in the top-level `data` blob
|
||||||
/// (a single rbxm containing all children from all services).
|
/// (a single rbxm containing all children from all services).
|
||||||
///
|
///
|
||||||
/// Each service's range in the blob is `child_count + ref_target_count`
|
/// Each service's range in the blob is `child_count + ref_target_count`
|
||||||
|
|
@ -37,10 +37,9 @@ pub const PROTOCOL_VERSION: u64 = 6;
|
||||||
/// to the service). The next `ref_target_count` are ObjectValue carriers
|
/// to the service). The next `ref_target_count` are ObjectValue carriers
|
||||||
/// whose `Value` property holds a Ref to the actual target.
|
/// whose `Value` property holds a Ref to the actual target.
|
||||||
///
|
///
|
||||||
/// `refs` maps property names to 1-based indices into the carrier slice
|
/// `refs` maps property names to 1-based indices into the carrier subrange
|
||||||
/// (the ObjectValue entries after the children). Index 1 is the first
|
/// (NOT the full service range). Index 1 = first carrier, index 2 = second,
|
||||||
/// carrier, 2 the second, etc. The server reads carriers via
|
/// etc. Index 0 or out-of-range values are ignored (treated as nil).
|
||||||
/// `carriers[idx - 1]`.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ServiceChunk {
|
pub struct ServiceChunk {
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,14 @@ impl SyncbackSignal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fire(&self, payload: SyncbackPayload) {
|
pub fn fire(&self, payload: SyncbackPayload) -> bool {
|
||||||
*self.payload.lock().unwrap_or_else(|e| e.into_inner()) = Some(payload);
|
let mut guard = self.payload.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
if guard.is_some() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*guard = Some(payload);
|
||||||
self.notify.notify_one();
|
self.notify.notify_one();
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn take_payload(&self) -> Option<SyncbackPayload> {
|
pub fn take_payload(&self) -> Option<SyncbackPayload> {
|
||||||
|
|
|
||||||
|
|
@ -786,3 +786,200 @@ fn roundtrip_build_syncback_rebuild() {
|
||||||
compare_trees(&dom_a, dom_a.root_ref(), &dom_b, dom_b.root_ref(), "root");
|
compare_trees(&dom_a, dom_a.root_ref(), &dom_b, dom_b.root_ref(), "root");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Additional edge-case parity tests ────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parity_children_with_attributes_and_tags() {
|
||||||
|
use rbx_dom_weak::types::{Attributes, Tags, Variant};
|
||||||
|
|
||||||
|
let mut attrs = Attributes::new();
|
||||||
|
attrs.insert("CustomFloat".to_string(), Variant::Float32(1.5));
|
||||||
|
attrs.insert("CustomBool".to_string(), Variant::Bool(true));
|
||||||
|
|
||||||
|
let mut tags = Tags::new();
|
||||||
|
tags.push("TestTag");
|
||||||
|
tags.push("AnotherTag");
|
||||||
|
|
||||||
|
let chunks = vec![make_service_chunk(
|
||||||
|
"ReplicatedStorage",
|
||||||
|
vec![
|
||||||
|
InstanceBuilder::new("Folder")
|
||||||
|
.with_name("TaggedFolder")
|
||||||
|
.with_property("Attributes", Variant::Attributes(attrs))
|
||||||
|
.with_property("Tags", Variant::Tags(tags)),
|
||||||
|
InstanceBuilder::new("ModuleScript")
|
||||||
|
.with_name("Utils")
|
||||||
|
.with_property("Source", Variant::String("return {}".into())),
|
||||||
|
],
|
||||||
|
)];
|
||||||
|
assert_live_matches_cli("live_syncback", chunks, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parity_multiple_cross_service_refs() {
|
||||||
|
use rbx_dom_weak::types::Variant;
|
||||||
|
|
||||||
|
let target_part = InstanceBuilder::new("Part")
|
||||||
|
.with_name("Target1")
|
||||||
|
.with_property("Anchored", Variant::Bool(true));
|
||||||
|
|
||||||
|
let target_folder = InstanceBuilder::new("Folder").with_name("Target2");
|
||||||
|
|
||||||
|
let obj_value_a = InstanceBuilder::new("ObjectValue")
|
||||||
|
.with_name("PointerA")
|
||||||
|
.with_property("Value", Variant::Ref(target_part.referent()));
|
||||||
|
|
||||||
|
let obj_value_b = InstanceBuilder::new("ObjectValue")
|
||||||
|
.with_name("PointerB")
|
||||||
|
.with_property("Value", Variant::Ref(target_folder.referent()));
|
||||||
|
|
||||||
|
let chunks = vec![
|
||||||
|
make_service_chunk("Workspace", vec![target_part, target_folder]),
|
||||||
|
make_service_chunk(
|
||||||
|
"ReplicatedStorage",
|
||||||
|
vec![InstanceBuilder::new("Folder")
|
||||||
|
.with_name("Refs")
|
||||||
|
.with_child(obj_value_a)
|
||||||
|
.with_child(obj_value_b)],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
assert_live_matches_cli("live_syncback", chunks, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parity_properties_only_no_children() {
|
||||||
|
use rbx_dom_weak::types::{Color3, Variant};
|
||||||
|
|
||||||
|
let chunks = vec![
|
||||||
|
make_service_chunk_full(
|
||||||
|
"Lighting",
|
||||||
|
vec![
|
||||||
|
("Ambient", Variant::Color3(Color3::new(0.1, 0.1, 0.1))),
|
||||||
|
("Brightness", Variant::Float32(1.5)),
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
),
|
||||||
|
make_service_chunk_full(
|
||||||
|
"SoundService",
|
||||||
|
vec![("DistanceFactor", Variant::Float32(8.0))],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
),
|
||||||
|
make_service_chunk_full(
|
||||||
|
"TextChatService",
|
||||||
|
vec![(
|
||||||
|
"ChatVersion",
|
||||||
|
Variant::Enum(rbx_dom_weak::types::Enum::from_u32(2)),
|
||||||
|
)],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
vec![],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
assert_live_matches_cli("live_syncback", chunks, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Resilience tests ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rapid_sequential_syncbacks() {
|
||||||
|
let (data_a, chunks_a) = build_syncback_request(vec![make_service_chunk(
|
||||||
|
"ReplicatedStorage",
|
||||||
|
vec![InstanceBuilder::new("Folder").with_name("RapidA")],
|
||||||
|
)]);
|
||||||
|
let (data_b, chunks_b) = build_syncback_request(vec![make_service_chunk(
|
||||||
|
"ReplicatedStorage",
|
||||||
|
vec![InstanceBuilder::new("Folder").with_name("RapidB")],
|
||||||
|
)]);
|
||||||
|
|
||||||
|
run_serve_test("live_syncback", |session, _| {
|
||||||
|
session.post_api_syncback(None, data_a, chunks_a);
|
||||||
|
session.wait_to_come_back_online();
|
||||||
|
|
||||||
|
session.post_api_syncback(None, data_b, chunks_b);
|
||||||
|
session.wait_to_come_back_online();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!session.path().join("src/shared/RapidA").exists(),
|
||||||
|
"RapidA should be gone after second syncback (clean mode)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
session.path().join("src/shared/RapidB").exists(),
|
||||||
|
"Second syncback should create RapidB"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recovers_from_malformed_chunks() {
|
||||||
|
run_serve_test("live_syncback", |session, _| {
|
||||||
|
let initial_info = session.get_api_rojo().unwrap();
|
||||||
|
|
||||||
|
let bad_chunk = librojo::web_api::ServiceChunk {
|
||||||
|
class_name: "ReplicatedStorage".to_string(),
|
||||||
|
child_count: 999,
|
||||||
|
ref_target_count: 0,
|
||||||
|
properties: std::collections::HashMap::new(),
|
||||||
|
refs: std::collections::HashMap::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = build_raw_syncback_body(None, &[], &[bad_chunk]);
|
||||||
|
let response = session.post_api_syncback_raw(body);
|
||||||
|
assert!(
|
||||||
|
response.status().is_success(),
|
||||||
|
"Server should accept the request (validation happens async), got {}",
|
||||||
|
response.status()
|
||||||
|
);
|
||||||
|
|
||||||
|
let new_info = session.wait_to_come_back_online();
|
||||||
|
assert_ne!(
|
||||||
|
initial_info.session_id, new_info.session_id,
|
||||||
|
"Server should have restarted (new session ID)"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
session.path().join("src").exists(),
|
||||||
|
"Filesystem should be intact (malformed syncback should not have written)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_raw_syncback_body(
|
||||||
|
place_id: Option<u64>,
|
||||||
|
data: &[u8],
|
||||||
|
services: &[librojo::web_api::ServiceChunk],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Req<'a> {
|
||||||
|
protocol_version: u64,
|
||||||
|
server_version: String,
|
||||||
|
place_id: Option<u64>,
|
||||||
|
#[serde(with = "serde_bytes")]
|
||||||
|
data: &'a [u8],
|
||||||
|
services: &'a [librojo::web_api::ServiceChunk],
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = Req {
|
||||||
|
protocol_version: 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)
|
||||||
|
.with_human_readable()
|
||||||
|
.with_struct_map();
|
||||||
|
request
|
||||||
|
.serialize(&mut serializer)
|
||||||
|
.expect("Failed to serialize syncback request");
|
||||||
|
body
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue