Enhance property encoding and class descriptor retrieval in rbx_dom_lua

- Introduced a new function, `findClassDescriptor`, to retrieve class properties and metadata, including inherited properties, from the database.
- Updated `encodeInstance` to include encoding for Tags alongside Attributes, ensuring comprehensive property serialization.
- Improved property filtering logic to skip non-serializable properties and those matching default values, aligning with syncback behavior.
- Enhanced comments for clarity on property handling and encoding processes.
This commit is contained in:
Joe 2026-01-30 13:18:20 -06:00
parent c0d089f0aa
commit dccd3a691a
6 changed files with 188 additions and 62 deletions

View file

@ -31,7 +31,7 @@ use crate::{
pub use file_names::{extension_for_middleware, name_for_inst, validate_file_name};
pub use fs_snapshot::FsSnapshot;
pub use hash::*;
pub use property_filter::{filter_properties, filter_properties_preallocated};
pub use property_filter::{filter_properties, filter_properties_preallocated, should_property_serialize};
pub use snapshot::{inst_path, SyncbackData, SyncbackSnapshot};
/// The name of an enviroment variable to use to override the behavior of

View file

@ -77,7 +77,9 @@ pub fn filter_properties_preallocated<'inst>(
}
}
fn should_property_serialize(class_name: &str, prop_name: &str) -> bool {
/// Checks if a property should serialize based on the reflection database.
/// Returns false for properties with DoesNotSerialize serialization, true otherwise.
pub fn should_property_serialize(class_name: &str, prop_name: &str) -> bool {
let database = rbx_reflection_database::get().unwrap();
let mut current_class_name = class_name;

View file

@ -20,7 +20,7 @@ use rbx_dom_weak::{
use crate::{
serve_session::ServeSession,
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
snapshot::{InstigatingSource, InstanceWithMeta, PatchSet, PatchUpdate},
web::{
interface::{
ErrorResponse, Instance, InstanceMetadata, MessagesPacket, OpenResponse, ReadResponse,
@ -392,14 +392,19 @@ impl ApiService {
)
})?;
// Get the parent Ref (required for top-level added instances)
let parent_ref = added
.parent
.context("Top-level added instance must have a parent")?;
// Find the parent instance to get its filesystem path
let parent_instance = tree
.get_instance(added.parent)
.get_instance(parent_ref)
.context("Parent instance not found in Rojo tree")?;
// Check if the parent path is unique (no duplicate-named siblings at any level)
// Uses pre-computed cache for O(d) instead of O(d × s) complexity
if !Self::is_tree_path_unique_with_cache(tree, added.parent, duplicate_siblings_cache) {
if !Self::is_tree_path_unique_with_cache(tree, parent_ref, duplicate_siblings_cache) {
anyhow::bail!(
"Cannot sync instance '{}' - parent path contains duplicate-named siblings",
added.name
@ -445,14 +450,32 @@ impl ApiService {
let instance_name = instance.name();
// Get the filesystem path from the instance's metadata
let instance_path = instance
// Get the instigating source and handle each variant appropriately
let instigating_source = instance
.metadata()
.instigating_source
.as_ref()
.map(|s| s.path())
.context("Instance has no filesystem path (not synced from filesystem)")?;
// Only delete instances that originate from filesystem paths, not from project nodes
let instance_path = match instigating_source {
InstigatingSource::Path(path) => path,
InstigatingSource::ProjectNode { name, .. } => {
// Instances defined in project files cannot be removed via syncback
// because that would require modifying the project file structure
log::warn!(
"Syncback: Cannot remove instance '{}' (id: {:?}) - it is defined in a project file",
name,
removed_id
);
anyhow::bail!(
"Cannot remove instance '{}' because it is defined in a project file. \
To remove this instance, edit the project file directly.",
name
);
}
};
// Check if path exists - it may have already been deleted as part of a parent removal
// (e.g., if both a directory and its children are marked for removal, the directory
// deletion will recursively delete all children, so we just skip them)
@ -832,7 +855,7 @@ impl ApiService {
// Check if there are any properties besides Source that need to be saved
// Use IndexMap for consistent ordering (like dedicated syncback)
let (properties, attributes) =
self.filter_properties_for_meta(&added.properties, Some("Source"));
self.filter_properties_for_meta(&added.class_name, &added.properties, Some("Source"));
if properties.is_empty() && attributes.is_empty() {
return Ok(());
@ -862,7 +885,7 @@ impl ApiService {
) -> anyhow::Result<()> {
use anyhow::Context;
let (properties, attributes) = self.filter_properties_for_meta(&added.properties, None);
let (properties, attributes) = self.filter_properties_for_meta(&added.class_name, &added.properties, None);
if properties.is_empty() && attributes.is_empty() {
return Ok(());
@ -891,7 +914,7 @@ impl ApiService {
) -> anyhow::Result<()> {
use anyhow::Context;
let (properties, attributes) = self.filter_properties_for_meta(&added.properties, None);
let (properties, attributes) = self.filter_properties_for_meta(&added.class_name, &added.properties, None);
// For non-Folder classes, we need to include className
let meta = self.build_meta_object(Some(&added.class_name), properties, attributes);
@ -915,20 +938,30 @@ impl ApiService {
/// Filters out:
/// - Ref and UniqueId properties (don't serialize to JSON)
/// - The skip_property if specified (e.g., "Source" for scripts)
/// - Properties that don't serialize (DoesNotSerialize in reflection database)
/// - Properties that match their default value
/// - Internal RBX* prefixed attributes
fn filter_properties_for_meta(
&self,
class_name: &str,
props: &HashMap<String, Variant>,
skip_property: Option<&str>,
) -> (
indexmap::IndexMap<String, serde_json::Value>,
indexmap::IndexMap<String, serde_json::Value>,
) {
use crate::syncback::should_property_serialize;
use crate::variant_eq::variant_eq;
use indexmap::IndexMap;
let mut properties: IndexMap<String, serde_json::Value> = IndexMap::new();
let mut attributes: IndexMap<String, serde_json::Value> = IndexMap::new();
// Get reflection database for default value comparison
let class_data = rbx_reflection_database::get()
.ok()
.and_then(|db| db.classes.get(class_name));
for (name, value) in props {
// Skip the specified property (e.g., Source for scripts)
if let Some(skip) = skip_property {
@ -962,6 +995,20 @@ impl ApiService {
continue;
}
// Check if property should serialize (matches dedicated syncback property_filter.rs)
if !should_property_serialize(class_name, name) {
continue;
}
// Skip properties that match their default value (matches dedicated syncback)
if let Some(data) = class_data {
if let Some(default) = data.default_properties.get(name.as_str()) {
if variant_eq(value, default) {
continue;
}
}
}
// Convert other properties
if let Some(json_value) = variant_to_json(value) {
properties.insert(name.clone(), json_value);
@ -1067,7 +1114,7 @@ impl ApiService {
// Filter properties, skipping Source (it's in the .luau file)
let (properties, attributes) =
self.filter_properties_for_meta(&added.properties, Some("Source"));
self.filter_properties_for_meta(&added.class_name, &added.properties, Some("Source"));
if properties.is_empty() && attributes.is_empty() {
return Ok(());
@ -1098,7 +1145,7 @@ impl ApiService {
use serde_json::json;
// Filter properties matching dedicated syncback behavior
let (properties, attributes) = self.filter_properties_for_meta(&added.properties, None);
let (properties, attributes) = self.filter_properties_for_meta(&added.class_name, &added.properties, None);
// Build the JSON model structure
// Format: https://rojo.space/docs/v7/sync-details/#json-models

View file

@ -253,7 +253,10 @@ pub struct ReadResponse<'a> {
#[serde(rename_all = "camelCase")]
pub struct AddedInstance {
/// The parent instance's Ref (must exist in Rojo's tree)
pub parent: Ref,
/// For top-level added instances, this is required.
/// For nested children, this can be omitted (parent is implied by nesting).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent: Option<Ref>,
/// The instance name
pub name: String,
/// The class name (e.g., "ModuleScript", "Script", "LocalScript")