From 5d86f69c792d848eb8436b1b0c691577da144606 Mon Sep 17 00:00:00 2001 From: Laurin Weger Date: Fri, 24 Oct 2025 16:46:34 +0200 Subject: [PATCH] fix svelte and apply diff issue --- .../verifier/src/orm/handle_backend_update.rs | 143 +++++++ engine/verifier/src/orm/process_changes.rs | 138 ++++++- .../src/app/pages/index.astro | 6 +- .../src/frontends/react/HelloWorld.tsx | 363 +++++++++--------- .../src/frontends/svelte/HelloWorld.svelte | 66 ++-- .../src/frontends/utils/flattenObject.ts | 94 +++-- .../signals/src/connector/applyDiff.test.ts | 12 + sdk/js/signals/src/connector/applyDiff.ts | 2 +- .../src/connector/ormConnectionHandler.ts | 6 + .../src/frontendAdapters/vue/useDeepSignal.ts | 2 + 10 files changed, 568 insertions(+), 264 deletions(-) diff --git a/engine/verifier/src/orm/handle_backend_update.rs b/engine/verifier/src/orm/handle_backend_update.rs index 395091ea..c41f0e9a 100644 --- a/engine/verifier/src/orm/handle_backend_update.rs +++ b/engine/verifier/src/orm/handle_backend_update.rs @@ -68,10 +68,29 @@ impl Verifier { }) .collect(); + log_info!( + "[orm_backend_update] called with #adds, #removes: {}, {}", + triple_inserts.len(), + triple_removes.len() + ); + + log_info!( + "[orm_backend_update] Total subscriptions scopes: {}", + self.orm_subscriptions.len() + ); + let mut scopes = vec![]; for (scope, subs) in self.orm_subscriptions.iter_mut() { // Remove old subscriptions + let initial_sub_count = subs.len(); subs.retain(|sub| !sub.sender.is_closed()); + let retained_sub_count = subs.len(); + log_info!( + "[orm_backend_update] Scope {:?}: {} subs ({} retained after cleanup)", + scope, + initial_sub_count, + retained_sub_count + ); if !(scope.target == NuriTargetV0::UserSite || scope @@ -80,9 +99,20 @@ impl Verifier { .map_or(false, |ol| overlaylink == *ol) || scope.target == NuriTargetV0::Repo(repo_id)) { + log_info!( + "[orm_backend_update] SKIPPING scope {:?} - does not match repo_id={:?} or overlay={:?}", + scope, + repo_id, + overlay_id + ); continue; } + log_info!( + "[orm_backend_update] PROCESSING scope {:?} - matches criteria", + scope + ); + // prepare to apply updates to tracked subjects and record the changes. let root_shapes_and_tracked_subjects = subs .iter() @@ -105,12 +135,29 @@ impl Verifier { "[orm_backend_update], creating patch objects for #scopes {}", scopes.len() ); + + if scopes.is_empty() { + log_info!("[orm_backend_update] NO SCOPES MATCHED - returning early without patches"); + return; + } + for (scope, shapes_zip) in scopes { let mut orm_changes: OrmChanges = HashMap::new(); + log_info!( + "[orm_backend_update] Processing scope {:?} with {} shape types", + scope, + shapes_zip.len() + ); + // Apply the changes to tracked subjects. for (root_shape_arc, all_shapes) in shapes_zip { let shape_iri = root_shape_arc.iri.clone(); + log_info!( + "[orm_backend_update] Calling process_changes_for_shape_and_session for shape={}, session={}", + shape_iri, + session_id + ); let _ = self.process_changes_for_shape_and_session( &scope, &shape_iri, @@ -121,9 +168,30 @@ impl Verifier { &mut orm_changes, false, ); + log_info!( + "[orm_backend_update] After process_changes_for_shape_and_session: orm_changes has {} shapes", + orm_changes.len() + ); + } + + log_info!( + "[orm_backend_update] Total orm_changes for scope: {} shapes with changes", + orm_changes.len() + ); + for (shape_iri, subject_changes) in &orm_changes { + log_info!( + "[orm_backend_update] Shape {}: {} subjects changed", + shape_iri, + subject_changes.len() + ); } let subs = self.orm_subscriptions.get_mut(&scope).unwrap(); + log_info!( + "[orm_backend_update] Processing {} subscriptions for this scope", + subs.len() + ); + for sub in subs.iter_mut() { log_debug!( "Applying changes to subscription with nuri {} and shape {}", @@ -158,7 +226,18 @@ impl Verifier { // Process changes for this subscription // Iterate over all changes and create patches + log_info!( + "[orm_backend_update] Iterating over {} shapes in orm_changes", + orm_changes.len() + ); + for (shape_iri, subject_changes) in &orm_changes { + log_info!( + "[orm_backend_update] Processing shape {}: {} subject changes", + shape_iri, + subject_changes.len() + ); + for (subject_iri, change) in subject_changes { log_debug!( "Patch creating for subject change x shape {} x {}. #changed preds: {}", @@ -174,6 +253,11 @@ impl Verifier { .map(|ts| ts.read().unwrap()) else { // We might not be tracking this subject x shape combination. Then, there is nothing to do. + log_info!( + "[orm_backend_update] SKIPPING subject {} x shape {} - not tracked in this subscription", + subject_iri, + shape_iri + ); continue; }; @@ -189,10 +273,21 @@ impl Verifier { && tracked_subject.valid == OrmTrackedSubjectValidity::Invalid { // Is the subject invalid and was it before? There is nothing we need to inform about. + log_info!( + "[orm_backend_update] SKIPPING subject {} - was and still is Invalid", + subject_iri + ); continue; } else if change.prev_valid == OrmTrackedSubjectValidity::Valid && tracked_subject.valid != OrmTrackedSubjectValidity::Valid { + log_info!( + "[orm_backend_update] Subject {} became invalid or untracked (prev={:?}, now={:?})", + subject_iri, + change.prev_valid, + tracked_subject.valid + ); + // Has the subject become invalid or untracked? // Check if any parent is also being deleted - if so, skip this deletion patch // because the parent deletion will implicitly delete the children @@ -202,6 +297,11 @@ impl Verifier { parent_ts.valid == OrmTrackedSubjectValidity::ToDelete }); + log_info!( + "[orm_backend_update] has_parent_being_deleted={}", + has_parent_being_deleted + ); + if !has_parent_being_deleted { // We add a patch, deleting the object at its root. // Start with an empty path - the subject IRI will be added in build_path_to_root_and_create_patches @@ -221,6 +321,14 @@ impl Verifier { ); } } else { + log_info!( + "[orm_backend_update] Subject {} is valid or became valid (prev={:?}, now={:?}), processing {} predicate changes", + subject_iri, + change.prev_valid, + tracked_subject.valid, + change.predicates.len() + ); + // The subject is valid or has become valid. // Process each predicate change for (_pred_iri, pred_change) in &change.predicates { @@ -238,6 +346,12 @@ impl Verifier { // Get the diff operations for this predicate change let diff_ops = create_diff_ops_from_predicate_change(pred_change); + log_info!( + "[orm_backend_update] Created {} diff_ops for predicate {}", + diff_ops.len(), + _pred_iri + ); + // For each diff operation, traverse up to the root to build the path for diff_op in diff_ops { let mut path = vec![pred_name.clone()]; @@ -261,6 +375,12 @@ impl Verifier { } } + log_info!( + "[orm_backend_update] Finished iterating shapes. Created {} patches, {} objects_to_create", + patches.len(), + objects_to_create.len() + ); + // Create patches for objects that need to be created // These are patches with {op: add, valType: object, value: Null, path: ...} // Sort by path length (shorter first) to ensure parent objects are created before children @@ -293,6 +413,20 @@ impl Verifier { } } + log_info!( + "[orm_backend_update] Created {} object_create_patches", + object_create_patches.len() + ); + + let total_patches = object_create_patches.len() + patches.len(); + log_info!( + "[orm_backend_update] SENDING {} total patches to frontend (session={}, nuri={}, shape={})", + total_patches, + session_id, + sub.nuri.repo(), + sub.shape_type.shape + ); + // Send response with patches. let _ = sub .sender @@ -302,10 +436,19 @@ impl Verifier { ))) .await; + log_info!("[orm_backend_update] Patches sent successfully"); + // Cleanup (remove tracked subjects to be deleted). Verifier::cleanup_tracked_subjects(sub); } + + log_info!( + "[orm_backend_update] Finished processing all subscriptions for scope {:?}", + scope + ); } + + log_info!("[orm_backend_update] COMPLETE - processed all scopes"); } } diff --git a/engine/verifier/src/orm/process_changes.rs b/engine/verifier/src/orm/process_changes.rs index e0617489..4005985a 100644 --- a/engine/verifier/src/orm/process_changes.rs +++ b/engine/verifier/src/orm/process_changes.rs @@ -96,18 +96,39 @@ impl Verifier { orm_changes: &mut OrmChanges, data_already_fetched: bool, ) -> Result<(), NgError> { + log_info!( + "[process_changes_for_shape_and_session] Starting processing for nuri, root_shape: {}, session: {}, {} shapes, {} triples added, {} triples removed, data_already_fetched: {}", + root_shape_iri, + session_id, + shapes.len(), + triples_added.len(), + triples_removed.len(), + data_already_fetched + ); + // First in, last out stack to keep track of objects to validate (nested objects first). Strings are object IRIs. let mut shape_validation_stack: Vec<(Arc, Vec)> = vec![]; // Track (shape_iri, subject_iri) pairs currently being validated to prevent cycles and double evaluation. let mut currently_validating: HashSet<(String, String)> = HashSet::new(); // Add root shape for first validation run. for shape in shapes { + log_info!( + "[process_changes_for_shape_and_session] Adding root shape to validation stack: {}", + shape.iri + ); shape_validation_stack.push((shape, vec![])); } // Process queue of shapes and subjects to validate. // For a given shape, we evaluate every subject against that shape. while let Some((shape, objects_to_validate)) = shape_validation_stack.pop() { + log_info!( + "[process_changes_for_shape_and_session] Processing shape from stack: {}, with {} objects to validate: {:?}", + shape.iri, + objects_to_validate.len(), + objects_to_validate + ); + // Collect triples relevant for validation. let added_triples_by_subject = group_by_subject_for_shape(&shape, triples_added, &objects_to_validate); @@ -118,13 +139,20 @@ impl Verifier { .chain(removed_triples_by_subject.keys()) .collect(); + log_info!( + "[process_changes_for_shape_and_session] Found {} modified subjects for shape {}: {:?}", + modified_subject_iris.len(), + shape.iri, + modified_subject_iris + ); + // Variable to collect nested objects that need validation. let mut nested_objects_to_eval: HashMap> = HashMap::new(); // For each subject, add/remove triples and validate. - log_debug!( - "processing modified subjects: {:?} against shape: {}", + log_info!( + "[process_changes_for_shape_and_session] processing modified subjects: {:?} against shape: {}", modified_subject_iris, shape.iri ); @@ -136,7 +164,7 @@ impl Verifier { // Cycle detection: Check if this (shape, subject) pair is already being validated if currently_validating.contains(&validation_key) { log_warn!( - "Cycle detected: subject '{}' with shape '{}' is already being validated. Marking as invalid.", + "[process_changes_for_shape_and_session] Cycle detected: subject '{}' with shape '{}' is already being validated. Marking as invalid.", subject_iri, shape.iri ); @@ -191,7 +219,7 @@ impl Verifier { }) .unwrap(); - log_debug!("[process_changes_for_shape_and_session] Creating change object for {}, {}", subject_iri, shape.iri); + log_info!("[process_changes_for_shape_and_session] Creating change object for {}, {}", subject_iri, shape.iri); let prev_valid = match orm_subscription .tracked_subjects .get(*subject_iri) @@ -225,9 +253,20 @@ impl Verifier { // If validation took place already, there's nothing more to do... if change.is_validated { + log_info!( + "[process_changes_for_shape_and_session] Subject {} already validated for shape {}, skipping", + subject_iri, + shape.iri + ); continue; } + log_info!( + "[process_changes_for_shape_and_session] Running validation for subject {} against shape {}", + subject_iri, + shape.iri + ); + // Run validation and record objects that need to be re-evaluated. { let orm_subscription = self @@ -247,21 +286,47 @@ impl Verifier { // We add the need_eval to be processed next after loop. // Filter out subjects already in the validation stack to prevent double evaluation. + log_info!( + "[process_changes_for_shape_and_session] Validation returned {} objects that need evaluation", + need_eval.len() + ); for (iri, schema_shape, needs_refetch) in need_eval { let eval_key = (schema_shape.clone(), iri.clone()); if !currently_validating.contains(&eval_key) { + log_info!( + "[process_changes_for_shape_and_session] Adding nested object to eval: {} with shape {}, needs_refetch: {}", + iri, + schema_shape, + needs_refetch + ); // Only add if not currently being validated nested_objects_to_eval .entry(schema_shape) .or_insert_with(Vec::new) .push((iri.clone(), needs_refetch)); + } else { + log_info!( + "[process_changes_for_shape_and_session] Skipping nested object {} with shape {} - already validating", + iri, + schema_shape + ); } } } } // Now, we queue all non-evaluated objects + log_info!( + "[process_changes_for_shape_and_session] Processing {} nested shape groups", + nested_objects_to_eval.len() + ); for (shape_iri, objects_to_eval) in &nested_objects_to_eval { + log_info!( + "[process_changes_for_shape_and_session] Processing nested shape: {} with {} objects", + shape_iri, + objects_to_eval.len() + ); + // Extract schema and shape Arc first (before any borrows) let schema = { let orm_sub = self.get_first_orm_subscription_for( @@ -275,29 +340,41 @@ impl Verifier { // Data might need to be fetched (if it has not been during initialization or nested shape fetch). if !data_already_fetched { - let objects_to_fetch = objects_to_eval + let objects_to_fetch: Vec = objects_to_eval .iter() .filter(|(_iri, needs_fetch)| *needs_fetch) .map(|(s, _)| s.clone()) .collect(); - // Create sparql query - let shape_query = - shape_type_to_sparql(&schema, &shape_iri, Some(objects_to_fetch))?; - let new_triples = - self.query_sparql_construct(shape_query, Some(nuri_to_string(nuri)))?; + log_info!( + "[process_changes_for_shape_and_session] Fetching data for {} objects that need refetch", + objects_to_fetch.len() + ); - // Recursively process nested objects. - self.process_changes_for_shape_and_session( - nuri, - &root_shape_iri, - [shape_arc.clone()].to_vec(), - session_id, - &new_triples, - &vec![], - orm_changes, - true, - )?; + if objects_to_fetch.len() > 0 { + // Create sparql query + let shape_query = + shape_type_to_sparql(&schema, &shape_iri, Some(objects_to_fetch))?; + let new_triples = + self.query_sparql_construct(shape_query, Some(nuri_to_string(nuri)))?; + + log_info!( + "[process_changes_for_shape_and_session] Fetched {} triples, recursively processing nested objects", + new_triples.len() + ); + + // Recursively process nested objects. + self.process_changes_for_shape_and_session( + nuri, + &root_shape_iri, + [shape_arc.clone()].to_vec(), + session_id, + &new_triples, + &vec![], + orm_changes, + true, + )?; + } } // Add objects @@ -307,16 +384,35 @@ impl Verifier { .map(|(s, _)| s.clone()) .collect(); if objects_not_to_fetch.len() > 0 { + log_info!( + "[process_changes_for_shape_and_session] Queueing {} objects that don't need fetching for shape {}", + objects_not_to_fetch.len(), + shape_iri + ); // Queue all objects that don't need fetching. shape_validation_stack.push((shape_arc, objects_not_to_fetch)); + } else { + log_info!( + "[process_changes_for_shape_and_session] No objects to queue for shape {} (all needed fetching)", + shape_iri + ); } } + + log_info!( + "[process_changes_for_shape_and_session] Cleaning up validation tracking for {} modified subjects", + modified_subject_iris.len() + ); for subject_iri in modified_subject_iris { let validation_key = (shape.iri.clone(), subject_iri.to_string()); currently_validating.remove(&validation_key); } } + log_info!( + "[process_changes_for_shape_and_session] Finished processing. Validation stack empty." + ); + Ok(()) } diff --git a/sdk/js/examples/multi-framework-signals/src/app/pages/index.astro b/sdk/js/examples/multi-framework-signals/src/app/pages/index.astro index c319cb98..ad75816a 100644 --- a/sdk/js/examples/multi-framework-signals/src/app/pages/index.astro +++ b/sdk/js/examples/multi-framework-signals/src/app/pages/index.astro @@ -5,7 +5,6 @@ import Highlight from "../components/Highlight.astro"; import VueRoot from "../components/VueRoot.vue"; import ReactRoot from "../components/ReactRoot"; import SvelteRoot from "../components/SvelteRoot.svelte"; -import { initNg } from "@ng-org/signals" const title = "Multi-framework app"; --- @@ -46,7 +45,8 @@ const title = "Multi-framework app"; - + + diff --git a/sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx b/sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx index da729d87..aa9b07a3 100644 --- a/sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx +++ b/sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx @@ -78,13 +78,11 @@ export function HelloWorldReact() { // @ts-expect-error window.reactState = state; - if (!state) return
Loading...
; // Create a table from the state object: One column for keys, one for values, one with an input to change the value. return (

Rendered in React

- -
- {objects.map((ormObj) => ( - - - - - - - - - - {(() => { - const setNestedValue = ( - obj: any, - path: string, - value: any - ) => { - const keys = path.split("."); - let current = obj; + {!state ? ( +
Loading...
+ ) : ( +
+ {objects.map((ormObj) => ( +
KeyValueEdit
+ + + + + + + + + {(() => { + const setNestedValue = ( + obj: any, + path: string, + value: any + ) => { + const keys = path.split("."); + let current = obj; - for (let i = 0; i < keys.length - 1; i++) { - current = current[keys[i]]; - } + for ( + let i = 0; + i < keys.length - 1; + i++ + ) { + current = current[keys[i]]; + } - current[keys[keys.length - 1]] = value; - }; + current[keys[keys.length - 1]] = value; + }; - const getNestedValue = ( - obj: any, - path: string - ) => { - return path - .split(".") - .reduce( - (current, key) => current[key], - obj - ); - }; + const getNestedValue = ( + obj: any, + path: string + ) => { + return path + .split(".") + .reduce( + (current, key) => current[key], + obj + ); + }; - return flattenObject(ormObj).map( - ([key, value]) => ( - - - - + + + - - ) - ); - })()} - -
KeyValueEdit
{key} - {value instanceof Set - ? Array.from(value).join( - ", " - ) - : Array.isArray(value) - ? `[${value.join(", ")}]` - : JSON.stringify(value)} - - {typeof value === "string" ? ( - { - setNestedValue( - ormObj, - key, - e.target.value - ); - }} - /> - ) : typeof value === - "number" ? ( - { - setNestedValue( - ormObj, - key, - Number( + return flattenObject(ormObj).map( + ([key, value]) => ( +
{key} + {value instanceof Set + ? Array.from( + value + ).join(", ") + : Array.isArray(value) + ? `[${value.join(", ")}]` + : JSON.stringify( + value + )} + + {typeof value === + "string" ? ( + { + setNestedValue( + ormObj, + key, e.target .value - ) - ); - }} - /> - ) : typeof value === - "boolean" ? ( - { - setNestedValue( - ormObj, - key, - e.target.checked - ); - }} - /> - ) : Array.isArray(value) ? ( -
- - -
- ) : value instanceof Set ? ( -
- - + +
+ ) : value instanceof Set ? ( +
+ + -
- ) : ( - "N/A" - )} -
- ))} -
+ ) { + currentSet.delete( + lastItem + ); + } + }} + > + Remove + +
+ ) : ( + "N/A" + )} + + + ) + ); + })()} + + + ))} + + )} ); } diff --git a/sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte b/sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte index f7e21fb6..b3485539 100644 --- a/sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte +++ b/sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte @@ -10,18 +10,18 @@ .split(".") .reduce((cur, k) => (cur == null ? cur : cur[k]), obj); } - function setNestedValue(obj: any, path: string, value: any) { - const keys = path.split("."); - let cur = obj; - for (let i = 0; i < keys.length - 1; i++) { - cur = cur[keys[i]]; - if (cur == null) return; - } - cur[keys[keys.length - 1]] = value; + function setNestedValue(targetObj: any, lastKey: string, value: any) { + // targetObj is the direct parent object containing the property + // lastKey is the property name to set + targetObj[lastKey] = value; } const flattenedObjects = $derived( $shapeObjects - ? Array.from($shapeObjects.values()).map((o) => flattenObject(o)) + ? Array.from($shapeObjects.values()).map((o) => { + const flattened = flattenObject(o); + (window as any).svelteFlattened = flattened; + return { entries: flattened, rootObj: o }; + }) : [] ); (window as any).svelteState = $shapeObjects; @@ -31,7 +31,7 @@

Rendered in Svelte

- {#each flattenedObjects as flatEntries} + {#each flattenedObjects as { entries: flatEntries, rootObj }} @@ -41,7 +41,7 @@ - {#each flatEntries as [key, value]} + {#each flatEntries as [key, value, lastKey, parentObj]}
{key} @@ -59,7 +59,7 @@ type="text" {value} oninput={(e: any) => - setNestedValue($shapeObjects, key, e.target.value)} + setNestedValue(parentObj, lastKey, e.target.value)} /> {:else if typeof value === "number"} setNestedValue( - $shapeObjects, - key, + parentObj, + lastKey, Number(e.target.value) )} /> @@ -77,24 +77,26 @@ type="checkbox" checked={value} onchange={(e: any) => - setNestedValue($shapeObjects, key, e.target.checked)} + setNestedValue(parentObj, lastKey, e.target.checked)} /> {:else if Array.isArray(value)}
@@ -102,21 +104,19 @@
diff --git a/sdk/js/examples/multi-framework-signals/src/frontends/utils/flattenObject.ts b/sdk/js/examples/multi-framework-signals/src/frontends/utils/flattenObject.ts index 1b55bf3a..bed55f73 100644 --- a/sdk/js/examples/multi-framework-signals/src/frontends/utils/flattenObject.ts +++ b/sdk/js/examples/multi-framework-signals/src/frontends/utils/flattenObject.ts @@ -1,43 +1,75 @@ interface FlattenOptions { - /** Maximum depth to traverse (default: 8). */ - maxDepth?: number; - /** Skip keys that start with a dollar sign (deepSignal meta). Default: true */ - skipDollarKeys?: boolean; + /** Maximum depth to traverse (default: 8). */ + maxDepth?: number; + /** Skip keys that start with a dollar sign (deepSignal meta). Default: true */ + skipDollarKeys?: boolean; } const isPlainObject = (v: any) => - Object.prototype.toString.call(v) === "[object Object]"; + Object.prototype.toString.call(v) === "[object Object]"; const flattenObject = ( - obj: any, - prefix = "", - options: FlattenOptions = {}, - seen = new Set(), - depth = 0 + obj: any, + prefix = "", + options: FlattenOptions = {}, + seen = new Set(), + depth = 0 ): Array<[string, any, string, any]> => { - const { maxDepth = 8, skipDollarKeys = true } = options; - const result: Array<[string, any, string, any]> = []; - if (!obj || typeof obj !== "object") return result; - if (seen.has(obj)) return result; // cycle detected - seen.add(obj); - if (depth > maxDepth) return result; + const { maxDepth = 8, skipDollarKeys = true } = options; + const result: Array<[string, any, string, any]> = []; + if (!obj || typeof obj !== "object") return result; + if (seen.has(obj)) return result; // cycle detected + seen.add(obj); + if (depth > maxDepth) return result; - for (const [key, value] of Object.entries(obj)) { - if (skipDollarKeys && key.startsWith("$")) continue; - const fullKey = prefix ? `${prefix}.${key}` : key; - if ( - value && - typeof value === "object" && - !Array.isArray(value) && - !(value instanceof Set) && - isPlainObject(value) - ) { - result.push(...flattenObject(value, fullKey, options, seen, depth + 1)); - } else { - result.push([fullKey, value, key, obj]); + for (const [key, value] of Object.entries(obj)) { + if (skipDollarKeys && key.startsWith("$")) continue; + const fullKey = prefix ? `${prefix}.${key}` : key; + + // Handle Sets containing objects with @id + if (value instanceof Set) { + const setItems = Array.from(value); + // Check if Set contains objects with @id + if ( + setItems.length > 0 && + setItems.some( + (item) => item && typeof item === "object" && "@id" in item + ) + ) { + // Flatten each object in the Set + setItems.forEach((item) => { + if (item && typeof item === "object" && "@id" in item) { + const itemId = item["@id"]; + const itemPrefix = `${fullKey}[@id=${itemId}]`; + result.push( + ...flattenObject( + item, + itemPrefix, + options, + seen, + depth + 1 + ) + ); + } + }); + } else { + // Set doesn't contain objects with @id, treat as leaf + result.push([fullKey, value, key, obj]); + } + } else if ( + value && + typeof value === "object" && + !Array.isArray(value) && + isPlainObject(value) + ) { + result.push( + ...flattenObject(value, fullKey, options, seen, depth + 1) + ); + } else { + result.push([fullKey, value, key, obj]); + } } - } - return result; + return result; }; export default flattenObject; diff --git a/sdk/js/signals/src/connector/applyDiff.test.ts b/sdk/js/signals/src/connector/applyDiff.test.ts index bcb5faa4..e787190f 100644 --- a/sdk/js/signals/src/connector/applyDiff.test.ts +++ b/sdk/js/signals/src/connector/applyDiff.test.ts @@ -127,6 +127,18 @@ describe("applyDiff - multi-valued objects (Set-based)", () => { expect(remaining["@id"]).toBe("urn:child2"); }); + test.only("remove object from root set", () => { + const obj1 = { "@id": "urn:child1", name: "Alice" }; + const obj2 = { "@id": "urn:child2", name: "Bob" }; + const state = new Set([ + { "@id": "urn:person1", children: [obj1] }, + { "@id": "urn:person2", children: [obj2] }, + ]); + const diff: Patch[] = [{ op: "remove", path: p("urn:person1") }]; + applyDiff(state, diff); + expect(state.size).toBe(1); + }); + test("create nested Set (multi-valued property within object in Set)", () => { const parent: any = { "@id": "urn:parent1" }; const state: any = { root: { parents: new Set([parent]) } }; diff --git a/sdk/js/signals/src/connector/applyDiff.ts b/sdk/js/signals/src/connector/applyDiff.ts index a99515f6..0dd25f90 100644 --- a/sdk/js/signals/src/connector/applyDiff.ts +++ b/sdk/js/signals/src/connector/applyDiff.ts @@ -232,7 +232,7 @@ export function applyDiff( } // Handle remove from Set - if (patch.op === "remove" && !patch.valType) { + if (patch.op === "remove" && patch.valType !== "set") { if (targetObj) { parentVal.delete(targetObj); } diff --git a/sdk/js/signals/src/connector/ormConnectionHandler.ts b/sdk/js/signals/src/connector/ormConnectionHandler.ts index 99338850..2848d03c 100644 --- a/sdk/js/signals/src/connector/ormConnectionHandler.ts +++ b/sdk/js/signals/src/connector/ormConnectionHandler.ts @@ -72,6 +72,7 @@ export class OrmConnection { ngSession.then(async ({ ng, session }) => { console.log("Creating orm connection. ng and session", ng, session); try { + await new Promise((resolve) => setTimeout(resolve, 4_000)); ng.orm_start( (scope.length == 0 ? "did:ng:" + session.private_store_id @@ -182,6 +183,11 @@ export class OrmConnection { this.ready = true; }; private onBackendUpdate = (patches: Patch[]) => { + console.log( + "connectionHandler: onBackendUpdate. Got patches:", + patches + ); + applyDiffToDeepSignal(this.signalObject, patches); }; diff --git a/sdk/js/signals/src/frontendAdapters/vue/useDeepSignal.ts b/sdk/js/signals/src/frontendAdapters/vue/useDeepSignal.ts index 24807a31..8d0b10ea 100644 --- a/sdk/js/signals/src/frontendAdapters/vue/useDeepSignal.ts +++ b/sdk/js/signals/src/frontendAdapters/vue/useDeepSignal.ts @@ -5,6 +5,8 @@ import { watch } from "@ng-org/alien-deepsignals"; * Bridge a deepSignal root into Vue with reactivity. * Uses a single version counter that increments on any deep mutation, * causing Vue to re-render when the deepSignal changes. + * + * TODO: Check performance and potentially improve. */ export function useDeepSignal(deepProxy: T): T { const version = ref(0);