fix svelte and apply diff issue

feat/orm-diffs
Laurin Weger 1 day ago
parent a4768e0340
commit 5d86f69c79
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 143
      engine/verifier/src/orm/handle_backend_update.rs
  2. 138
      engine/verifier/src/orm/process_changes.rs
  3. 6
      sdk/js/examples/multi-framework-signals/src/app/pages/index.astro
  4. 363
      sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx
  5. 66
      sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte
  6. 94
      sdk/js/examples/multi-framework-signals/src/frontends/utils/flattenObject.ts
  7. 12
      sdk/js/signals/src/connector/applyDiff.test.ts
  8. 2
      sdk/js/signals/src/connector/applyDiff.ts
  9. 6
      sdk/js/signals/src/connector/ormConnectionHandler.ts
  10. 2
      sdk/js/signals/src/frontendAdapters/vue/useDeepSignal.ts

@ -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");
}
}

@ -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<OrmSchemaShape>, Vec<String>)> = 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<ShapeIri, Vec<(SubjectIri, bool)>> =
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<String> = 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(())
}

@ -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";
<ReactRoot client:only="react" />
</Highlight>
<!-- <Highlight svelte>
<Highlight svelte>
<SvelteRoot client:only />
</Highlight> -->
</Highlight>
</Layout>

@ -78,13 +78,11 @@ export function HelloWorldReact() {
// @ts-expect-error
window.reactState = state;
if (!state) return <div>Loading...</div>;
// Create a table from the state object: One column for keys, one for values, one with an input to change the value.
return (
<div>
<p>Rendered in React</p>
<button
onClick={() => {
window.ng.sparql_update(
@ -97,196 +95,211 @@ export function HelloWorldReact() {
Add example data
</button>
<div>
{objects.map((ormObj) => (
<table border={1} cellPadding={5} key={ormObj["@id"]}>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
{(() => {
const setNestedValue = (
obj: any,
path: string,
value: any
) => {
const keys = path.split(".");
let current = obj;
{!state ? (
<div>Loading...</div>
) : (
<div>
{objects.map((ormObj) => (
<table border={1} cellPadding={5} key={ormObj["@id"]}>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
{(() => {
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]) => (
<tr key={key}>
<td>{key}</td>
<td>
{value instanceof Set
? Array.from(value).join(
", "
)
: Array.isArray(value)
? `[${value.join(", ")}]`
: JSON.stringify(value)}
</td>
<td>
{typeof value === "string" ? (
<input
type="text"
value={value}
onChange={(e) => {
setNestedValue(
ormObj,
key,
e.target.value
);
}}
/>
) : typeof value ===
"number" ? (
<input
type="number"
value={value}
onChange={(e) => {
setNestedValue(
ormObj,
key,
Number(
return flattenObject(ormObj).map(
([key, value]) => (
<tr key={key}>
<td>{key}</td>
<td>
{value instanceof Set
? Array.from(
value
).join(", ")
: Array.isArray(value)
? `[${value.join(", ")}]`
: JSON.stringify(
value
)}
</td>
<td>
{typeof value ===
"string" ? (
<input
type="text"
value={value}
onChange={(e) => {
setNestedValue(
ormObj,
key,
e.target
.value
)
);
}}
/>
) : typeof value ===
"boolean" ? (
<input
type="checkbox"
checked={value}
onChange={(e) => {
setNestedValue(
ormObj,
key,
e.target.checked
);
}}
/>
) : Array.isArray(value) ? (
<div>
<button
onClick={() => {
const currentArray =
getNestedValue(
ormObj,
key
);
);
}}
/>
) : typeof value ===
"number" ? (
<input
type="number"
value={value}
onChange={(e) => {
setNestedValue(
ormObj,
key,
[
...currentArray,
currentArray.length +
1,
]
Number(
e.target
.value
)
);
}}
>
Add
</button>
<button
onClick={() => {
const currentArray =
getNestedValue(
ormObj,
key
);
if (
currentArray.length >
0
) {
/>
) : typeof value ===
"boolean" ? (
<input
type="checkbox"
checked={value}
onChange={(e) => {
setNestedValue(
ormObj,
key,
e.target
.checked
);
}}
/>
) : Array.isArray(value) ? (
<div>
<button
onClick={() => {
const currentArray =
getNestedValue(
ormObj,
key
);
setNestedValue(
ormObj,
key,
currentArray.slice(
0,
-1
)
);
}
}}
>
Remove
</button>
</div>
) : value instanceof Set ? (
<div>
<button
onClick={() => {
const currentSet =
getNestedValue(
ormObj,
key
[
...currentArray,
currentArray.length +
1,
]
);
currentSet.add(
`item${currentSet.size + 1}`
);
}}
>
Add
</button>
<button
onClick={() => {
const currentSet =
getNestedValue(
ormObj,
key
}}
>
Add
</button>
<button
onClick={() => {
const currentArray =
getNestedValue(
ormObj,
key
);
if (
currentArray.length >
0
) {
setNestedValue(
ormObj,
key,
currentArray.slice(
0,
-1
)
);
}
}}
>
Remove
</button>
</div>
) : value instanceof Set ? (
<div>
<button
onClick={() => {
const currentSet =
getNestedValue(
ormObj,
key
);
currentSet.add(
`item${currentSet.size + 1}`
);
const lastItem =
Array.from(
currentSet
).pop();
if (lastItem) {
currentSet.delete(
}}
>
Add
</button>
<button
onClick={() => {
const currentSet =
getNestedValue(
ormObj,
key
);
const lastItem =
Array.from(
currentSet
).pop();
if (
lastItem
);
}
}}
>
Remove
</button>
</div>
) : (
"N/A"
)}
</td>
</tr>
)
);
})()}
</tbody>
</table>
))}
</div>
) {
currentSet.delete(
lastItem
);
}
}}
>
Remove
</button>
</div>
) : (
"N/A"
)}
</td>
</tr>
)
);
})()}
</tbody>
</table>
))}
</div>
)}
</div>
);
}

@ -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 @@
<div>
<p>Rendered in Svelte</p>
{#each flattenedObjects as flatEntries}
{#each flattenedObjects as { entries: flatEntries, rootObj }}
<table border="1" cellpadding="5">
<thead>
<tr>
@ -41,7 +41,7 @@
</tr>
</thead>
<tbody>
{#each flatEntries as [key, value]}
{#each flatEntries as [key, value, lastKey, parentObj]}
<tr>
<td style="white-space:nowrap;">{key}</td>
<td>
@ -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"}
<input
@ -67,8 +67,8 @@
{value}
oninput={(e: any) =>
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)}
<div style="display:flex; gap:.5rem;">
<button
onclick={() => {
const cur = getNestedValue($shapeObjects, key) || [];
setNestedValue($shapeObjects, key, [
...cur,
cur.length + 1,
setNestedValue(parentObj, lastKey, [
...value,
value.length + 1,
]);
}}>Add</button
>
<button
onclick={() => {
const cur = getNestedValue($shapeObjects, key) || [];
if (cur.length)
setNestedValue($shapeObjects, key, cur.slice(0, -1));
if (value.length)
setNestedValue(
parentObj,
lastKey,
value.slice(0, -1)
);
}}>Remove</button
>
</div>
@ -102,21 +104,19 @@
<div style="display:flex; gap:.5rem;">
<button
onclick={() => {
const cur: Set<any> = getNestedValue(
$shapeObjects,
key
);
cur.add(`item${cur.size + 1}`);
const newSet = new Set(value);
newSet.add(`item${newSet.size + 1}`);
setNestedValue(parentObj, lastKey, newSet);
}}>Add</button
>
<button
onclick={() => {
const cur: Set<any> = getNestedValue(
$shapeObjects,
key
);
const last = Array.from(cur).pop();
if (last !== undefined) cur.delete(last);
const arr = Array.from(value);
const last = arr.pop();
if (last !== undefined) {
const newSet = new Set(arr);
setNestedValue(parentObj, lastKey, newSet);
}
}}>Remove</button
>
</div>

@ -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<any>(),
depth = 0
obj: any,
prefix = "",
options: FlattenOptions = {},
seen = new Set<any>(),
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;

@ -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]) } };

@ -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);
}

@ -72,6 +72,7 @@ export class OrmConnection<T extends BaseType> {
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<T extends BaseType> {
this.ready = true;
};
private onBackendUpdate = (patches: Patch[]) => {
console.log(
"connectionHandler: onBackendUpdate. Got patches:",
patches
);
applyDiffToDeepSignal(this.signalObject, patches);
};

@ -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<T>(deepProxy: T): T {
const version = ref(0);

Loading…
Cancel
Save