patches working

feat/orm-diffs
Laurin Weger 3 days ago
parent 390a49e7f4
commit 29905f5ab8
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 174
      engine/verifier/src/orm/handle_frontend_update.rs
  2. 14
      engine/verifier/src/orm/query.rs
  3. 2
      engine/verifier/src/request_processor.rs
  4. 18
      sdk/js/alien-deepsignals/src/deepSignal.ts
  5. 24
      sdk/js/alien-deepsignals/src/test/deepSignalOptions.test.ts
  6. 66
      sdk/js/alien-deepsignals/src/test/watchPatches.test.ts
  7. 11
      sdk/js/examples/multi-framework-signals/src/app/pages/index.astro
  8. 7
      sdk/js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx
  9. 2
      sdk/js/examples/multi-framework-signals/src/frontends/svelte/HelloWorld.svelte
  10. 37
      sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.schema.ts
  11. 9
      sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.shapeTypes.ts
  12. 22
      sdk/js/examples/multi-framework-signals/src/shapes/orm/basic.typings.ts
  13. 8
      sdk/js/examples/multi-framework-signals/src/shapes/shex/basic.shex
  14. 18
      sdk/js/lib-wasm/src/lib.rs
  15. 59
      sdk/js/signals/src/connector/ormConnectionHandler.ts
  16. 12
      sdk/js/signals/src/frontendAdapters/react/useShape.ts
  17. 28
      sdk/rust/src/tests/orm_apply_patches.rs

@ -40,12 +40,15 @@ impl Verifier {
let (mut sender, _orm_subscription) = let (mut sender, _orm_subscription) =
self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?; self.get_first_orm_subscription_sender_for(scope, Some(&shape_iri), Some(&session_id))?;
log_info!("[orm_update_self] got subscription");
// Revert changes, if there. // Revert changes, if there.
if revert_inserts.len() > 0 || revert_removes.len() > 0 { if revert_inserts.len() > 0 || revert_removes.len() > 0 {
let revert_changes = GraphQuadsPatch { let revert_changes = GraphQuadsPatch {
inserts: revert_removes, inserts: revert_removes,
removes: revert_inserts, removes: revert_inserts,
}; };
log_info!("[orm_frontend_update] Reverting");
// TODO: Call with correct params. // TODO: Call with correct params.
// self.orm_backend_update(session_id, scope, "", revert_changes) // self.orm_backend_update(session_id, scope, "", revert_changes)
@ -63,7 +66,7 @@ impl Verifier {
diff: OrmPatches, diff: OrmPatches,
) -> Result<(), String> { ) -> Result<(), String> {
log_info!( log_info!(
"frontend_update_orm session={} shape={} diff={:?}", "[orm_frontend_update] session={} shape={} diff={:?}",
session_id, session_id,
shape_iri, shape_iri,
diff diff
@ -74,12 +77,17 @@ impl Verifier {
self.get_first_orm_subscription_for(scope, Some(&shape_iri), Some(&session_id)); self.get_first_orm_subscription_for(scope, Some(&shape_iri), Some(&session_id));
let doc_nuri = orm_subscription.nuri.clone(); let doc_nuri = orm_subscription.nuri.clone();
log_info!("[orm_frontend_update] got subscription");
let sparql_update = create_sparql_update_query_for_diff(orm_subscription, diff); let sparql_update = create_sparql_update_query_for_diff(orm_subscription, diff);
log_info!(
"[orm_frontend_update] created sparql_update query:\n{}",
sparql_update
);
(doc_nuri, sparql_update) (doc_nuri, sparql_update)
}; };
log_debug!("Created SPARQL query for patches:\n{}", sparql_update);
match self match self
.process_sparql_update( .process_sparql_update(
&doc_nuri, &doc_nuri,
@ -90,8 +98,17 @@ impl Verifier {
) )
.await .await
{ {
Err(e) => Err(e), Err(e) => {
log_info!("[orm_frontend_update] query failed");
Err(e)
}
Ok((_, revert_inserts, revert_removes, skolemnized_blank_nodes)) => { Ok((_, revert_inserts, revert_removes, skolemnized_blank_nodes)) => {
log_info!(
"[orm_frontend_update] query successful. Reverts? {}",
revert_inserts.len()
);
if !revert_inserts.is_empty() if !revert_inserts.is_empty()
|| !revert_removes.is_empty() || !revert_removes.is_empty()
|| !skolemnized_blank_nodes.is_empty() || !skolemnized_blank_nodes.is_empty()
@ -117,6 +134,11 @@ fn create_sparql_update_query_for_diff(
orm_subscription: &OrmSubscription, orm_subscription: &OrmSubscription,
diff: OrmPatches, diff: OrmPatches,
) -> String { ) -> String {
log_info!(
"[create_sparql_update_query_for_diff] Starting with {} patches",
diff.len()
);
// First sort patches. // First sort patches.
// - Process delete patches first. // - Process delete patches first.
// - Process object creation add operations before rest, to ensure potential blank nodes are created. // - Process object creation add operations before rest, to ensure potential blank nodes are created.
@ -124,6 +146,11 @@ fn create_sparql_update_query_for_diff(
.iter() .iter()
.filter(|patch| patch.op == OrmPatchOp::remove) .filter(|patch| patch.op == OrmPatchOp::remove)
.collect(); .collect();
log_info!(
"[create_sparql_update_query_for_diff] Found {} delete patches",
delete_patches.len()
);
let add_object_patches: Vec<_> = diff let add_object_patches: Vec<_> = diff
.iter() .iter()
.filter(|patch| { .filter(|patch| {
@ -134,7 +161,12 @@ fn create_sparql_update_query_for_diff(
} }
}) })
.collect(); .collect();
let add_literal_patches: Vec<_> = diff log_info!(
"[create_sparql_update_query_for_diff] Found {} add object patches",
add_object_patches.len()
);
let add_primitive_patches: Vec<_> = diff
.iter() .iter()
.filter(|patch| { .filter(|patch| {
patch.op == OrmPatchOp::add patch.op == OrmPatchOp::add
@ -144,19 +176,33 @@ fn create_sparql_update_query_for_diff(
} }
}) })
.collect(); .collect();
log_info!(
"[create_sparql_update_query_for_diff] Found {} add primitive patches",
add_primitive_patches.len()
);
// For each diff op, we create a separate INSERT or DELETE block. // For each diff op, we create a separate INSERT or DELETE block.
let mut sparql_sub_queries: Vec<String> = vec![]; let mut sparql_sub_queries: Vec<String> = vec![];
// Create delete statements. // Create delete statements.
// //
for del_patch in delete_patches.iter() { for (idx, del_patch) in delete_patches.iter().enumerate() {
log_info!(
"[create_sparql_update_query_for_diff] Processing delete patch {}/{}: path={}",
idx + 1,
delete_patches.len(),
del_patch.path
);
let mut var_counter: i32 = 0; let mut var_counter: i32 = 0;
let (where_statements, target, _pred_schema) = let (where_statements, target, _pred_schema) =
create_where_statements_for_patch(&del_patch, &mut var_counter, &orm_subscription); create_where_statements_for_patch(&del_patch, &mut var_counter, &orm_subscription);
let (subject_var, target_predicate, target_object) = target; let (subject_var, target_predicate, target_object) = target;
log_info!("[create_sparql_update_query_for_diff] Delete patch where_statements: {:?}, subject_var={}, target_predicate={}, target_object={:?}",
where_statements, subject_var, target_predicate, target_object);
let delete_statement; let delete_statement;
if let Some(target_object) = target_object { if let Some(target_object) = target_object {
// Delete the link to exactly one object (IRI referenced in path, i.e. target_object) // Delete the link to exactly one object (IRI referenced in path, i.e. target_object)
@ -183,29 +229,47 @@ fn create_sparql_update_query_for_diff(
delete_statement, delete_statement,
where_statements.join(" .\n ") where_statements.join(" .\n ")
)); ));
log_info!(
"[create_sparql_update_query_for_diff] Added delete query #{}",
sparql_sub_queries.len()
);
} }
// Process add object patches (might need blank nodes) // Process add object patches (might need blank nodes)
// //
for _add_obj_patch in add_object_patches { for (idx, _add_obj_patch) in add_object_patches.iter().enumerate() {
log_info!("[create_sparql_update_query_for_diff] Processing add object patch {}/{} (NOT YET IMPLEMENTED)", idx + 1, add_object_patches.len());
// Creating objects without an id field is only supported in one circumstance: // Creating objects without an id field is only supported in one circumstance:
// An object is added to a property which has a max cardinality of one, e.g. `painting.artist`. // An object is added to a property which has a max cardinality of one, e.g. `painting.artist`.
// In that case, we create a blank node. // In that case, we create a blank node.
// TODO: We need to set up a list of created blank nodes and where they belong to. // TODO: We need to set up a list of created blank nodes and where they belong to.
// POTENTIAL PANIC SOURCE: This is not implemented yet
} }
// Process literal add patches // Process primitive add patches
// //
for add_patch in add_literal_patches { for (idx, add_patch) in add_primitive_patches.iter().enumerate() {
log_info!(
"[create_sparql_update_query_for_diff] Processing add primitive patch {}/{}: path={}",
idx + 1,
add_primitive_patches.len(),
add_patch.path
);
let mut var_counter: i32 = 0; let mut var_counter: i32 = 0;
// Create WHERE statements from path. // Create WHERE statements from path.
// POTENTIAL PANIC SOURCE: create_where_statements_for_patch can panic in several places
let (where_statements, target, pred_schema) = let (where_statements, target, pred_schema) =
create_where_statements_for_patch(&add_patch, &mut var_counter, &orm_subscription); create_where_statements_for_patch(&add_patch, &mut var_counter, &orm_subscription);
let (subject_var, target_predicate, target_object) = target; let (subject_var, target_predicate, target_object) = target;
log_info!("[create_sparql_update_query_for_diff] Add patch where_statements: {:?}, subject_var={}, target_predicate={}, target_object={:?}",
where_statements, subject_var, target_predicate, target_object);
if let Some(_target_object) = target_object { if let Some(_target_object) = target_object {
// Reference to exactly one object found. This is invalid when inserting literals. // Reference to exactly one object found. This is invalid when inserting literals.
log_info!("[create_sparql_update_query_for_diff] SKIPPING: target_object found for literal add (invalid)");
// TODO: Return error? // TODO: Return error?
continue; continue;
} else { } else {
@ -215,6 +279,7 @@ fn create_sparql_update_query_for_diff(
Some(val) => json_to_sparql_val(&val), // Can be one or more (joined with ", "). Some(val) => json_to_sparql_val(&val), // Can be one or more (joined with ", ").
None => { None => {
// A value must be set. This patch is invalid. // A value must be set. This patch is invalid.
log_info!("[create_sparql_update_query_for_diff] SKIPPING: No value in add patch (invalid)");
// TODO: Return error? // TODO: Return error?
continue; continue;
} }
@ -225,6 +290,7 @@ fn create_sparql_update_query_for_diff(
// If the schema only has max one value, // If the schema only has max one value,
// then `add` can also overwrite values, so we need to delete the previous one // then `add` can also overwrite values, so we need to delete the previous one
if !pred_schema.unwrap().is_multi() { if !pred_schema.unwrap().is_multi() {
log_info!("[create_sparql_update_query_for_diff] Single-value predicate, adding DELETE before INSERT");
let remove_statement = let remove_statement =
format!(" {} <{}> ?o{}", subject_var, target_predicate, var_counter); format!(" {} <{}> ?o{}", subject_var, target_predicate, var_counter);
@ -236,6 +302,10 @@ fn create_sparql_update_query_for_diff(
remove_statement, remove_statement,
wheres.join(" .\n ") wheres.join(" .\n ")
)); ));
log_info!(
"[create_sparql_update_query_for_diff] Added delete query #{}",
sparql_sub_queries.len()
);
// var_counter += 1; // Not necessary because not used afterwards. // var_counter += 1; // Not necessary because not used afterwards.
} }
// The actual INSERT. // The actual INSERT.
@ -245,9 +315,17 @@ fn create_sparql_update_query_for_diff(
add_statement, add_statement,
where_statements.join(". \n ") where_statements.join(". \n ")
)); ));
log_info!(
"[create_sparql_update_query_for_diff] Added insert query #{}",
sparql_sub_queries.len()
);
} }
} }
log_info!(
"[create_sparql_update_query_for_diff] Finished. Generated {} sub-queries",
sparql_sub_queries.len()
);
return sparql_sub_queries.join(";\n"); return sparql_sub_queries.join(";\n");
} }
@ -294,6 +372,12 @@ fn create_where_statements_for_patch(
(String, String, Option<String>), (String, String, Option<String>),
Option<Arc<OrmSchemaPredicate>>, Option<Arc<OrmSchemaPredicate>>,
) { ) {
log_info!(
"[create_where_statements_for_patch] Starting. patch.path={}, patch.op={:?}",
patch.path,
patch.op
);
let mut body_statements: Vec<String> = vec![]; let mut body_statements: Vec<String> = vec![];
let mut where_statements: Vec<String> = vec![]; let mut where_statements: Vec<String> = vec![];
@ -303,9 +387,20 @@ fn create_where_statements_for_patch(
.map(|s| decode_json_pointer(&s.to_string())) .map(|s| decode_json_pointer(&s.to_string()))
.collect(); .collect();
log_info!(
"[create_where_statements_for_patch] Decoded path into {} segments: {:?}",
path.len(),
path
);
path.remove(0);
// Handle special case: The whole object is deleted. // Handle special case: The whole object is deleted.
if path.len() == 1 { if path.len() == 1 {
let root_iri = &path[0]; let root_iri = &path[0];
log_info!(
"[create_where_statements_for_patch] Special case: whole object deletion for root_iri={}",
root_iri
);
body_statements.push(format!("<{}> ?p ?o", root_iri)); body_statements.push(format!("<{}> ?p ?o", root_iri));
where_statements.push(format!("<{}> ?p ?o", root_iri)); where_statements.push(format!("<{}> ?p ?o", root_iri));
return ( return (
@ -315,24 +410,56 @@ fn create_where_statements_for_patch(
); );
} }
log_info!(
"[create_where_statements_for_patch] Getting root schema for shape={}",
orm_subscription.shape_type.shape
);
let subj_schema: &Arc<OrmSchemaShape> = orm_subscription let subj_schema: &Arc<OrmSchemaShape> = orm_subscription
.shape_type .shape_type
.schema .schema
.get(&orm_subscription.shape_type.shape) .get(&orm_subscription.shape_type.shape)
.unwrap(); .unwrap();
log_info!("[create_where_statements_for_patch] Root schema found");
let mut current_subj_schema: Arc<OrmSchemaShape> = subj_schema.clone(); let mut current_subj_schema: Arc<OrmSchemaShape> = subj_schema.clone();
// The root IRI might change, if the parent path segment was an IRI. // The root IRI might change, if the parent path segment was an IRI.
let root_iri = path.remove(0); let root_iri = path.remove(0);
let mut subject_ref = format!("<{}>", root_iri); let mut subject_ref = format!("<{}>", root_iri);
log_info!(
"[create_where_statements_for_patch] Starting traversal from root_iri={}, remaining path segments={}",
root_iri,
path.len()
);
while path.len() > 0 { while path.len() > 0 {
let pred_name = path.remove(0); let pred_name = path.remove(0);
log_info!(
"[create_where_statements_for_patch] Processing path segment: pred_name={}, remaining={}",
pred_name,
path.len()
);
// POTENTIAL PANIC SOURCE: find_pred_schema_by_name can panic
log_info!(
"[create_where_statements_for_patch] Looking up predicate schema for name={}",
pred_name
);
let pred_schema = find_pred_schema_by_name(&pred_name, &current_subj_schema); let pred_schema = find_pred_schema_by_name(&pred_name, &current_subj_schema);
log_info!(
"[create_where_statements_for_patch] Found predicate schema: iri={}, is_object={}, is_multi={}",
pred_schema.iri,
pred_schema.is_object(),
pred_schema.is_multi()
);
// Case: We arrived at a leaf value. // Case: We arrived at a leaf value.
if path.len() == 0 { if path.len() == 0 {
log_info!(
"[create_where_statements_for_patch] Reached leaf value. Returning target: subject_ref={}, predicate={}",
subject_ref,
pred_schema.iri
);
return ( return (
where_statements, where_statements,
(subject_ref, pred_schema.iri.clone(), None), (subject_ref, pred_schema.iri.clone(), None),
@ -346,6 +473,12 @@ fn create_where_statements_for_patch(
"{} <{}> ?o{}", "{} <{}> ?o{}",
subject_ref, pred_schema.iri, var_counter, subject_ref, pred_schema.iri, var_counter,
)); ));
log_info!(
"[create_where_statements_for_patch] Added where statement for nested object: {} <{}> ?o{}",
subject_ref,
pred_schema.iri,
var_counter
);
// Update the subject_ref for traversal (e.g. <bob> <hasCat> ?o1 . ?o1 <type> Cat); // Update the subject_ref for traversal (e.g. <bob> <hasCat> ?o1 . ?o1 <type> Cat);
subject_ref = format!("?o{}", var_counter); subject_ref = format!("?o{}", var_counter);
@ -358,9 +491,19 @@ fn create_where_statements_for_patch(
); );
} }
if pred_schema.is_multi() { if pred_schema.is_multi() {
log_info!("[create_where_statements_for_patch] Predicate is multi-valued, expecting object IRI in path");
let object_iri = path.remove(0); let object_iri = path.remove(0);
log_info!(
"[create_where_statements_for_patch] Got object_iri={}, remaining path={}",
object_iri,
path.len()
);
// Path ends on an object IRI, which we return here as well. // Path ends on an object IRI, which we return here as well.
if path.len() == 0 { if path.len() == 0 {
log_info!(
"[create_where_statements_for_patch] Path ends on object IRI. Returning target with object={}",
object_iri
);
return ( return (
where_statements, where_statements,
(subject_ref, pred_schema.iri.clone(), Some(object_iri)), (subject_ref, pred_schema.iri.clone(), Some(object_iri)),
@ -368,21 +511,36 @@ fn create_where_statements_for_patch(
); );
} }
// POTENTIAL PANIC SOURCE: get_first_child_schema can panic
log_info!(
"[create_where_statements_for_patch] Getting child schema for object_iri={}",
object_iri
);
current_subj_schema = current_subj_schema =
get_first_child_schema(Some(&object_iri), &pred_schema, &orm_subscription); get_first_child_schema(Some(&object_iri), &pred_schema, &orm_subscription);
log_info!("[create_where_statements_for_patch] Child schema found");
// Since we have new IRI that we can use as root, we replace the current one with it. // Since we have new IRI that we can use as root, we replace the current one with it.
subject_ref = format!("<{object_iri}>"); subject_ref = format!("<{object_iri}>");
// And can clear all, now unnecessary where statements. // And can clear all, now unnecessary where statements.
where_statements.clear(); where_statements.clear();
log_info!(
"[create_where_statements_for_patch] Reset subject_ref to <{}> and cleared where statements",
object_iri
);
} else { } else {
// Set to child subject schema. // Set to child subject schema.
// TODO: Actually, we should get the tracked subject and check for the correct shape there. // TODO: Actually, we should get the tracked subject and check for the correct shape there.
// As long as there is only one allowed shape or the first one is valid, this is fine. // As long as there is only one allowed shape or the first one is valid, this is fine.
log_info!("[create_where_statements_for_patch] Predicate is single-valued, getting child schema");
// POTENTIAL PANIC SOURCE: get_first_child_schema can panic
current_subj_schema = get_first_child_schema(None, &pred_schema, &orm_subscription); current_subj_schema = get_first_child_schema(None, &pred_schema, &orm_subscription);
log_info!("[create_where_statements_for_patch] Child schema found");
} }
} }
// Can't happen. // Can't happen.
log_err!("[create_where_statements_for_patch] PANIC: Reached end of function unexpectedly (should be impossible)");
panic!(); panic!();
} }

@ -35,15 +35,14 @@ impl Verifier {
// &update.overlay_id, // &update.overlay_id,
// ); // );
//let base = NuriV0::repo_id(&repo.id); //let base = NuriV0::repo_id(&repo.id);
let binding = nuri.unwrap();
let nuri = binding.split_at(53).0;
log_info!("querying construct\n{}\n{}\n", nuri, query); let nuri_str = nuri.as_ref().map(|s| s.as_str());
log_debug!("querying construct\n{}\n{}\n", nuri_str.unwrap(), query);
let parsed = Query::parse(&query, Some(nuri.clone())) let parsed =
.map_err(|e| NgError::OxiGraphError(e.to_string()))?; Query::parse(&query, nuri_str).map_err(|e| NgError::OxiGraphError(e.to_string()))?;
let results = oxistore let results = oxistore
.query(parsed, Some(nuri.to_string())) .query(parsed, nuri)
.map_err(|e| NgError::OxiGraphError(e.to_string()))?; .map_err(|e| NgError::OxiGraphError(e.to_string()))?;
match results { match results {
QueryResults::Graph(triples) => { QueryResults::Graph(triples) => {
@ -51,8 +50,7 @@ impl Verifier {
for t in triples { for t in triples {
match t { match t {
Err(e) => { Err(e) => {
log_info!("Error: {:?}n", e); log_err!("{}", e.to_string());
return Err(NgError::SparqlError(e.to_string())); return Err(NgError::SparqlError(e.to_string()));
} }
Ok(triple) => { Ok(triple) => {

@ -15,11 +15,9 @@ use std::sync::Arc;
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::SinkExt; use futures::SinkExt;
use futures::StreamExt; use futures::StreamExt;
use ng_net::actor::SoS;
use ng_net::types::InboxPost; use ng_net::types::InboxPost;
use ng_net::types::NgQRCode; use ng_net::types::NgQRCode;
use ng_net::types::NgQRCodeProfileSharingV0; use ng_net::types::NgQRCodeProfileSharingV0;
use ng_oxigraph::oxigraph::sparql::EvaluationError;
use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults}; use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults};
use ng_oxigraph::oxrdf::{Literal, NamedNode, Quad, Term}; use ng_oxigraph::oxrdf::{Literal, NamedNode, Quad, Term};
use ng_oxigraph::oxsdatatypes::DateTime; use ng_oxigraph::oxsdatatypes::DateTime;

@ -11,6 +11,16 @@ import { computed, signal, isSignal } from "./core";
/** A batched deep mutation (set/add/remove) from a deepSignal root. */ /** A batched deep mutation (set/add/remove) from a deepSignal root. */
export type DeepPatch = { export type DeepPatch = {
/** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */
path: (string | number)[];
} & (
| DeepSetAddPatch
| DeepSetRemovePatch
| DeepObjectAddPatch
| DeepRemovePatch
| DeepLiteralAddPatch
);
export type DeepPatchInternal = {
/** Unique identifier for the deep signal root which produced this patch. */ /** Unique identifier for the deep signal root which produced this patch. */
root: symbol; root: symbol;
/** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */ /** Property path (array indices, object keys, synthetic Set entry ids) from the root to the mutated location. */
@ -22,6 +32,7 @@ export type DeepPatch = {
| DeepRemovePatch | DeepRemovePatch
| DeepLiteralAddPatch | DeepLiteralAddPatch
); );
export interface DeepSetAddPatch { export interface DeepSetAddPatch {
/** Mutation kind applied at the resolved `path`. */ /** Mutation kind applied at the resolved `path`. */
op: "add"; op: "add";
@ -105,7 +116,7 @@ function buildPath(
return path; return path;
} }
function queuePatch(patch: DeepPatch) { function queuePatch(patch: DeepPatchInternal) {
if (!pendingPatches) pendingPatches = new Map(); if (!pendingPatches) pendingPatches = new Map();
const root = patch.root; const root = patch.root;
let list = pendingPatches.get(root); let list = pendingPatches.get(root);
@ -113,6 +124,9 @@ function queuePatch(patch: DeepPatch) {
list = []; list = [];
pendingPatches.set(root, list); pendingPatches.set(root, list);
} }
// Remove root, we do not send that back.
// @ts-ignore
delete patch.root;
list.push(patch); list.push(patch);
if (!microtaskScheduled) { if (!microtaskScheduled) {
microtaskScheduled = true; microtaskScheduled = true;
@ -124,7 +138,7 @@ function queuePatch(patch: DeepPatch) {
for (const [rootId, patches] of groups) { for (const [rootId, patches] of groups) {
if (!patches.length) continue; if (!patches.length) continue;
const subs = mutationSubscribers.get(rootId); const subs = mutationSubscribers.get(rootId);
if (subs) subs.forEach((cb) => cb(patches)); if (subs) subs.forEach((callback) => callback(patches));
} }
}); });
} }

@ -1,5 +1,9 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { deepSignal, DeepPatch, DeepSignalOptions } from "../deepSignal"; import {
deepSignal,
DeepPatchInternal,
DeepSignalOptions,
} from "../deepSignal";
import { watch } from "../watch"; import { watch } from "../watch";
describe("deepSignal options", () => { describe("deepSignal options", () => {
@ -12,7 +16,7 @@ describe("deepSignal options", () => {
}; };
const state = deepSignal({ data: {} as any }, options); const state = deepSignal({ data: {} as any }, options);
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -51,7 +55,7 @@ describe("deepSignal options", () => {
}; };
const state = deepSignal({ s: new Set<any>() }, options); const state = deepSignal({ s: new Set<any>() }, options);
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -76,7 +80,7 @@ describe("deepSignal options", () => {
}; };
const state = deepSignal({ root: {} as any }, options); const state = deepSignal({ root: {} as any }, options);
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -119,7 +123,7 @@ describe("deepSignal options", () => {
}; };
const state = deepSignal({ items: [] as any[] }, options); const state = deepSignal({ items: [] as any[] }, options);
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -148,7 +152,7 @@ describe("deepSignal options", () => {
}; };
const state = deepSignal({ s: new Set<any>() }, options); const state = deepSignal({ s: new Set<any>() }, options);
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -210,7 +214,7 @@ describe("deepSignal options", () => {
}; };
const state = deepSignal({ container: {} as any }, options); const state = deepSignal({ container: {} as any }, options);
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -283,7 +287,7 @@ describe("deepSignal options", () => {
describe("backward compatibility", () => { describe("backward compatibility", () => {
it("still works without options", async () => { it("still works without options", async () => {
const state = deepSignal({ data: { value: 1 } }); const state = deepSignal({ data: { value: 1 } });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -298,7 +302,7 @@ describe("deepSignal options", () => {
// TODO: Delete duplicate logic for `id`. Only accept @id. // TODO: Delete duplicate logic for `id`. Only accept @id.
it("objects with id property still work for Sets", async () => { it("objects with id property still work for Sets", async () => {
const state = deepSignal({ s: new Set<any>() }); const state = deepSignal({ s: new Set<any>() });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -315,7 +319,7 @@ describe("deepSignal options", () => {
it("@id takes precedence over id property", async () => { it("@id takes precedence over id property", async () => {
const state = deepSignal({ s: new Set<any>() }); const state = deepSignal({ s: new Set<any>() });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );

@ -3,14 +3,14 @@ import {
deepSignal, deepSignal,
setSetEntrySyntheticId, setSetEntrySyntheticId,
addWithId, addWithId,
DeepPatch, DeepPatchInternal,
} from "../deepSignal"; } from "../deepSignal";
import { watch, observe } from "../watch"; import { watch, observe } from "../watch";
describe("watch (patch mode)", () => { describe("watch (patch mode)", () => {
it("emits set patches with correct paths and batching", async () => { it("emits set patches with correct paths and batching", async () => {
const state = deepSignal({ a: { b: 1 }, arr: [1, { x: 2 }] }); const state = deepSignal({ a: { b: 1 }, arr: [1, { x: 2 }] });
const received: DeepPatch[][] = []; const received: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) => { const { stopListening: stop } = watch(state, ({ patches }) => {
received.push(patches); received.push(patches);
}); });
@ -34,7 +34,7 @@ describe("watch (patch mode)", () => {
a: { b: 1 }, a: { b: 1 },
c: 2, c: 2,
}); });
const out: DeepPatch[][] = []; const out: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) => const { stopListening: stop } = watch(state, ({ patches }) =>
out.push(patches) out.push(patches)
); );
@ -52,8 +52,8 @@ describe("watch (patch mode)", () => {
it("observe patch mode mirrors watch patch mode", async () => { it("observe patch mode mirrors watch patch mode", async () => {
const state = deepSignal({ a: 1 }); const state = deepSignal({ a: 1 });
const wp: DeepPatch[][] = []; const wp: DeepPatchInternal[][] = [];
const ob: DeepPatch[][] = []; const ob: DeepPatchInternal[][] = [];
const { stopListening: stop1 } = watch(state, ({ patches }) => const { stopListening: stop1 } = watch(state, ({ patches }) =>
wp.push(patches) wp.push(patches)
); );
@ -72,7 +72,7 @@ describe("watch (patch mode)", () => {
it("filters out patches from other roots", async () => { it("filters out patches from other roots", async () => {
const a = deepSignal({ x: 1 }); const a = deepSignal({ x: 1 });
const b = deepSignal({ y: 2 }); const b = deepSignal({ y: 2 });
const out: DeepPatch[][] = []; const out: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(a, ({ patches }) => const { stopListening: stop } = watch(a, ({ patches }) =>
out.push(patches) out.push(patches)
); );
@ -86,7 +86,7 @@ describe("watch (patch mode)", () => {
it("emits patches for Set structural mutations (add/delete)", async () => { it("emits patches for Set structural mutations (add/delete)", async () => {
const state = deepSignal<{ s: Set<number> }>({ s: new Set([1, 2]) }); const state = deepSignal<{ s: Set<number> }>({ s: new Set([1, 2]) });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) => const { stopListening: stop } = watch(state, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -110,7 +110,7 @@ describe("watch (patch mode)", () => {
it("emits patches for nested objects added after initialization", async () => { it("emits patches for nested objects added after initialization", async () => {
const state = deepSignal<{ root: any }>({ root: {} }); const state = deepSignal<{ root: any }>({ root: {} });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -124,7 +124,7 @@ describe("watch (patch mode)", () => {
it("emits patches for deeply nested arrays and objects", async () => { it("emits patches for deeply nested arrays and objects", async () => {
const state = deepSignal<{ data: any }>({ data: null }); const state = deepSignal<{ data: any }>({ data: null });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -161,7 +161,7 @@ describe("watch (patch mode)", () => {
it("emits patches for Set with nested objects added as one operation", async () => { it("emits patches for Set with nested objects added as one operation", async () => {
const state = deepSignal<{ container: any }>({ container: {} }); const state = deepSignal<{ container: any }>({ container: {} });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) => const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -188,7 +188,7 @@ describe("watch (patch mode)", () => {
const innerA = new Set<any>([{ id: "node1", x: 1 }]); const innerA = new Set<any>([{ id: "node1", x: 1 }]);
const s = new Set<any>([innerA]); const s = new Set<any>([innerA]);
const state = deepSignal<{ graph: Set<any> }>({ graph: s }); const state = deepSignal<{ graph: Set<any> }>({ graph: s });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) => const { stopListening: stop } = watch(state, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -204,7 +204,7 @@ describe("watch (patch mode)", () => {
it("tracks deep nested object mutation inside a Set entry after iteration", async () => { it("tracks deep nested object mutation inside a Set entry after iteration", async () => {
const rawEntry = { id: "n1", data: { val: 1 } }; const rawEntry = { id: "n1", data: { val: 1 } };
const st = deepSignal({ bag: new Set<any>([rawEntry]) }); const st = deepSignal({ bag: new Set<any>([rawEntry]) });
const collected: DeepPatch[][] = []; const collected: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
collected.push(patches) collected.push(patches)
); );
@ -215,7 +215,9 @@ describe("watch (patch mode)", () => {
} }
proxied.data.val = 2; proxied.data.val = 2;
await Promise.resolve(); await Promise.resolve();
const flat = collected.flat().map((p: DeepPatch) => p.path.join(".")); const flat = collected
.flat()
.map((p: DeepPatchInternal) => p.path.join("."));
expect(flat.some((p: string) => p.endsWith("n1.data.val"))).toBe(true); expect(flat.some((p: string) => p.endsWith("n1.data.val"))).toBe(true);
stop(); stop();
}); });
@ -223,13 +225,15 @@ describe("watch (patch mode)", () => {
it("allows custom synthetic id for Set entry", async () => { it("allows custom synthetic id for Set entry", async () => {
const node = { name: "x" }; const node = { name: "x" };
const state = deepSignal({ s: new Set<any>() }); const state = deepSignal({ s: new Set<any>() });
const collected2: DeepPatch[][] = []; const collected2: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches }) => const { stopListening: stop } = watch(state, ({ patches }) =>
collected2.push(patches) collected2.push(patches)
); );
addWithId(state.s as any, node, "custom123"); addWithId(state.s as any, node, "custom123");
await Promise.resolve(); await Promise.resolve();
const flat = collected2.flat().map((p: DeepPatch) => p.path.join(".")); const flat = collected2
.flat()
.map((p: DeepPatchInternal) => p.path.join("."));
expect(flat.some((p: string) => p === "s.custom123")).toBe(true); expect(flat.some((p: string) => p === "s.custom123")).toBe(true);
stop(); stop();
}); });
@ -237,7 +241,7 @@ describe("watch (patch mode)", () => {
describe("Set", () => { describe("Set", () => {
it("emits patches for primitive adds", async () => { it("emits patches for primitive adds", async () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -266,7 +270,7 @@ describe("watch (patch mode)", () => {
}); });
it("emits patches for primitive deletes", async () => { it("emits patches for primitive deletes", async () => {
const st = deepSignal({ s: new Set<any>([true, 2, "3"]) }); const st = deepSignal({ s: new Set<any>([true, 2, "3"]) });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -293,7 +297,7 @@ describe("watch (patch mode)", () => {
}); });
it("does not emit patches for non-existent primitives", async () => { it("does not emit patches for non-existent primitives", async () => {
const st = deepSignal({ s: new Set<any>([1, 2]) }); const st = deepSignal({ s: new Set<any>([1, 2]) });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -306,7 +310,7 @@ describe("watch (patch mode)", () => {
}); });
it("does not emit patches for already added primitive", async () => { it("does not emit patches for already added primitive", async () => {
const st = deepSignal({ s: new Set<any>([1, "test", true]) }); const st = deepSignal({ s: new Set<any>([1, "test", true]) });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -322,7 +326,7 @@ describe("watch (patch mode)", () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
addWithId(st.s as any, { id: "a", x: 1 }, "a"); addWithId(st.s as any, { id: "a", x: 1 }, "a");
addWithId(st.s as any, { id: "b", x: 2 }, "b"); addWithId(st.s as any, { id: "b", x: 2 }, "b");
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -340,7 +344,7 @@ describe("watch (patch mode)", () => {
it("emits delete patch for object entry", async () => { it("emits delete patch for object entry", async () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
const obj = { id: "n1", x: 1 }; const obj = { id: "n1", x: 1 };
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) => const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -356,7 +360,7 @@ describe("watch (patch mode)", () => {
}); });
it("does not emit patch for duplicate add", async () => { it("does not emit patch for duplicate add", async () => {
const st = deepSignal({ s: new Set<number>([1]) }); const st = deepSignal({ s: new Set<number>([1]) });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) => const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -367,7 +371,7 @@ describe("watch (patch mode)", () => {
}); });
it("does not emit patch deleting non-existent entry", async () => { it("does not emit patch deleting non-existent entry", async () => {
const st = deepSignal({ s: new Set<number>([1]) }); const st = deepSignal({ s: new Set<number>([1]) });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) => const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -378,7 +382,7 @@ describe("watch (patch mode)", () => {
}); });
it("addWithId primitive returns primitive and emits patch with primitive key", async () => { it("addWithId primitive returns primitive and emits patch with primitive key", async () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) => const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -396,7 +400,7 @@ describe("watch (patch mode)", () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
const obj = { name: "x" }; const obj = { name: "x" };
setSetEntrySyntheticId(obj, "customX"); setSetEntrySyntheticId(obj, "customX");
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) => const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -413,7 +417,7 @@ describe("watch (patch mode)", () => {
{ id: "e1", inner: { v: 1 } }, { id: "e1", inner: { v: 1 } },
"e1" "e1"
); );
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -429,7 +433,7 @@ describe("watch (patch mode)", () => {
it("raw reference mutation produces no deep patch while proxied does", async () => { it("raw reference mutation produces no deep patch while proxied does", async () => {
const raw = { id: "id1", data: { x: 1 } }; const raw = { id: "id1", data: { x: 1 } };
const st = deepSignal({ s: new Set<any>([raw]) }); const st = deepSignal({ s: new Set<any>([raw]) });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -451,7 +455,7 @@ describe("watch (patch mode)", () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
const a1 = { id: "dup", v: 1 }; const a1 = { id: "dup", v: 1 };
const a2 = { id: "dup", v: 2 }; const a2 = { id: "dup", v: 2 };
const patches: DeepPatch[][] = []; const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) => const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
@ -483,7 +487,7 @@ describe("watch (patch mode)", () => {
expect(arr[0].inner.v).toBe(1); expect(arr[0].inner.v).toBe(1);
const spread = [...st.s]; const spread = [...st.s];
expect(spread[0].inner.v).toBe(1); expect(spread[0].inner.v).toBe(1);
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -498,7 +502,7 @@ describe("watch (patch mode)", () => {
describe("Arrays & mixed batch", () => { describe("Arrays & mixed batch", () => {
it("emits patches for splice/unshift/shift in single batch", async () => { it("emits patches for splice/unshift/shift in single batch", async () => {
const st = deepSignal({ arr: [1, 2, 3] }); const st = deepSignal({ arr: [1, 2, 3] });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );
@ -512,7 +516,7 @@ describe("watch (patch mode)", () => {
}); });
it("mixed object/array/Set mutations batch together", async () => { it("mixed object/array/Set mutations batch together", async () => {
const st = deepSignal({ o: { a: 1 }, arr: [1], s: new Set<any>() }); const st = deepSignal({ o: { a: 1 }, arr: [1], s: new Set<any>() });
const batches: DeepPatch[][] = []; const batches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
batches.push(patches) batches.push(patches)
); );

@ -28,21 +28,24 @@ const title = "Multi-framework app";
let info = await ng.client_info(); let info = await ng.client_info();
console.log(info.V0.details); console.log(info.V0.details);
initNg(ng, event.session); initNg(ng, event.session);
window.ng = ng;
}, },
true, true,
[] []
); );
</script> </script>
<Layout title={title}> <Layout title={title}>
<Highlight vue> <!-- <Highlight vue>
<VueRoot client:only /> <VueRoot client:only />
</Highlight> </Highlight> -->
<Highlight react> <Highlight react>
<ReactRoot client:only="react" /> <ReactRoot client:only="react" />
</Highlight> </Highlight>
<!--
<Highlight svelte> <Highlight svelte>
<SvelteRoot client:only /> <SvelteRoot client:only />
</Highlight> </Highlight> -->
</Layout> </Layout>

@ -2,9 +2,10 @@ import React from "react";
import { useShape } from "@ng-org/signals/react"; import { useShape } from "@ng-org/signals/react";
import flattenObject from "../utils/flattenObject"; import flattenObject from "../utils/flattenObject";
import { TestObjectShapeType } from "../../shapes/orm/testShape.shapeTypes"; import { TestObjectShapeType } from "../../shapes/orm/testShape.shapeTypes";
import { BasicShapeType } from "../../shapes/orm/basic.shapeTypes";
export function HelloWorldReact() { export function HelloWorldReact() {
const state = useShape(TestObjectShapeType)?.entries().next(); const state = [...(useShape(BasicShapeType)?.entries() || [])][0];
// @ts-expect-error // @ts-expect-error
window.reactState = state; window.reactState = state;
@ -17,14 +18,14 @@ export function HelloWorldReact() {
<div> <div>
<p>Rendered in React</p> <p>Rendered in React</p>
<button {/* <button
onClick={() => { onClick={() => {
state.boolValue = !state.boolValue; state.boolValue = !state.boolValue;
state.numValue += 2; state.numValue += 2;
}} }}
> >
click me to change multiple props click me to change multiple props
</button> </button> */}
<table border={1} cellPadding={5}> <table border={1} cellPadding={5}>
<thead> <thead>

@ -21,7 +21,7 @@
} }
const flatEntries = $derived( const flatEntries = $derived(
$shapeObject $shapeObject
? flattenObject($shapeObject.entries().next() || ({} as any)) ? $shapeObject.entries().map((o) => flattenObject(o)[0] || ({} as any))
: [] : []
); );
$effect(() => { $effect(() => {

@ -0,0 +1,37 @@
import type { Schema } from "@ng-org/shex-orm";
/**
* =============================================================================
* basicSchema: Schema for basic
* =============================================================================
*/
export const basicSchema: Schema = {
"http://example.org/Basic": {
iri: "http://example.org/Basic",
predicates: [
{
dataTypes: [
{
valType: "literal",
literals: ["http://example.org/Basic"],
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
readablePredicate: "@type",
},
{
dataTypes: [
{
valType: "string",
},
],
maxCardinality: 1,
minCardinality: 1,
iri: "http://example.org/basicString",
readablePredicate: "basicString",
},
],
},
};

@ -0,0 +1,9 @@
import type { ShapeType } from "@ng-org/shex-orm";
import { basicSchema } from "./basic.schema";
import type { Basic } from "./basic.typings";
// ShapeTypes for basic
export const BasicShapeType: ShapeType<Basic> = {
schema: basicSchema,
shape: "http://example.org/Basic",
};

@ -0,0 +1,22 @@
export type IRI = string;
/**
* =============================================================================
* Typescript Typings for basic
* =============================================================================
*/
/**
* Basic Type
*/
export interface Basic {
readonly "@id": IRI;
/**
* Original IRI: http://www.w3.org/1999/02/22-rdf-syntax-ns#type
*/
"@type": string;
/**
* Original IRI: http://example.org/basicString
*/
basicString: string;
}

@ -0,0 +1,8 @@
PREFIX ex: <http://example.org/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
ex:Basic {
a [ ex:Basic ] ;
ex:basicString xsd:string ;
}

@ -21,6 +21,7 @@ use std::sync::Arc;
use nextgraph::net::app_protocol::AppRequest; use nextgraph::net::app_protocol::AppRequest;
use ng_net::orm::OrmPatch; use ng_net::orm::OrmPatch;
use ng_repo::log_info;
use ng_wallet::types::SensitiveWallet; use ng_wallet::types::SensitiveWallet;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -1314,7 +1315,9 @@ async fn app_request_stream_(
// list. // list.
// }; // };
// }; // };
let response_js = serde_wasm_bindgen::to_value(&app_response).unwrap(); let response_js = app_response
.serialize(&serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true))
.unwrap();
// if let Some(graph_triples) = graph_triples_js { // if let Some(graph_triples) = graph_triples_js {
// let response: Object = response_js.try_into().map_err(|_| { // let response: Object = response_js.try_into().map_err(|_| {
// "Error while adding triples to AppResponse.V0.State".to_string() // "Error while adding triples to AppResponse.V0.State".to_string()
@ -1818,7 +1821,6 @@ pub async fn orm_start(
) -> Result<JsValue, String> { ) -> Result<JsValue, String> {
let shape_type: OrmShapeType = serde_wasm_bindgen::from_value::<OrmShapeType>(shapeType) let shape_type: OrmShapeType = serde_wasm_bindgen::from_value::<OrmShapeType>(shapeType)
.map_err(|e| format!("Deserialization error of shapeType {e}"))?; .map_err(|e| format!("Deserialization error of shapeType {e}"))?;
log_info!("frontend_orm_start {:?}", shape_type);
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id) let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?; .map_err(|_| "Deserialization error of session_id".to_string())?;
let scope = if scope.is_empty() { let scope = if scope.is_empty() {
@ -1826,6 +1828,7 @@ pub async fn orm_start(
} else { } else {
NuriV0::new_from(&scope).map_err(|_| "Deserialization error of scope".to_string())? NuriV0::new_from(&scope).map_err(|_| "Deserialization error of scope".to_string())?
}; };
log_info!("[orm_start] parameters parsed, calling new_orm_start");
let mut request = AppRequest::new_orm_start(scope, shape_type); let mut request = AppRequest::new_orm_start(scope, shape_type);
request.set_session_id(session_id); request.set_session_id(session_id);
app_request_stream_(request, callback).await app_request_stream_(request, callback).await
@ -1833,21 +1836,24 @@ pub async fn orm_start(
#[wasm_bindgen] #[wasm_bindgen]
pub async fn orm_update( pub async fn orm_update(
scope: JsValue, scope: String,
shapeTypeName: String, shapeTypeName: String,
diff: JsValue, diff: JsValue,
session_id: JsValue, session_id: JsValue,
) -> Result<(), String> { ) -> Result<(), String> {
let diff: OrmPatches = serde_wasm_bindgen::from_value::<OrmPatches>(diff) let diff: OrmPatches = serde_wasm_bindgen::from_value::<OrmPatches>(diff)
.map_err(|e| format!("Deserialization error of diff {e}"))?; .map_err(|e| format!("Deserialization error of diff {e}"))?;
log_info!("frontend_update_orm {:?}", diff);
let scope: NuriV0 = serde_wasm_bindgen::from_value::<NuriV0>(scope) let scope = if scope.is_empty() {
.map_err(|_| "Deserialization error of scope".to_string())?; NuriV0::new_entire_user_site()
} else {
NuriV0::new_from(&scope).map_err(|_| "Deserialization error of scope".to_string())?
};
let mut request = AppRequest::new_orm_update(scope, shapeTypeName, diff); let mut request = AppRequest::new_orm_update(scope, shapeTypeName, diff);
let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id) let session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?; .map_err(|_| "Deserialization error of session_id".to_string())?;
request.set_session_id(session_id); request.set_session_id(session_id);
log_info!("[orm_update] calling orm_update");
let response = nextgraph::local_broker::app_request(request) let response = nextgraph::local_broker::app_request(request)
.await .await
.map_err(|e: NgError| e.to_string())?; .map_err(|e: NgError| e.to_string())?;

@ -72,14 +72,10 @@ export class OrmConnection<T extends BaseType> {
ngSession.then(({ ng, session }) => { ngSession.then(({ ng, session }) => {
console.log("ng and session", ng, session); console.log("ng and session", ng, session);
try { try {
const sc = ("did:ng:" + session.private_store_id).substring(
0,
53
);
console.log("calling orm_start with nuri", sc);
ng.orm_start( ng.orm_start(
sc, (scope.length == 0
? "did:ng:" + session.private_store_id
: scope) as string,
shapeType, shapeType,
session.session_id, session.session_id,
this.onBackendMessage this.onBackendMessage
@ -97,10 +93,10 @@ export class OrmConnection<T extends BaseType> {
* @param ng * @param ng
* @returns * @returns
*/ */
public static getConnection<T extends BaseType>( public static getConnection = <T extends BaseType>(
shapeType: ShapeType<T>, shapeType: ShapeType<T>,
scope: Scope scope: Scope
): OrmConnection<T> { ): OrmConnection<T> => {
const scopeKey = canonicalScope(scope); const scopeKey = canonicalScope(scope);
// Unique identifier for a given shape type and scope. // Unique identifier for a given shape type and scope.
@ -118,50 +114,51 @@ export class OrmConnection<T extends BaseType> {
OrmConnection.idToEntry.set(identifier, newConnection); OrmConnection.idToEntry.set(identifier, newConnection);
return newConnection; return newConnection;
} }
} };
public release() { public release = () => {
if (this.refCount > 0) this.refCount--; if (this.refCount > 0) this.refCount--;
if (this.refCount === 0) { if (this.refCount === 0) {
OrmConnection.idToEntry.delete(this.identifier); OrmConnection.idToEntry.delete(this.identifier);
OrmConnection.cleanupSignalRegistry?.unregister(this.signalObject); OrmConnection.cleanupSignalRegistry?.unregister(this.signalObject);
} }
} };
private onSignalObjectUpdate({ patches }: WatchPatchEvent<Set<T>>) { private onSignalObjectUpdate = ({ patches }: WatchPatchEvent<Set<T>>) => {
if (this.suspendDeepWatcher || !this.ready || !patches.length) return; if (this.suspendDeepWatcher || !this.ready || !patches.length) return;
const ormPatches = deepPatchesToDiff(patches); const ormPatches = deepPatchesToDiff(patches);
ngSession.then(({ ng, session }) => { ngSession.then(({ ng, session }) => {
ng.orm_update( ng.orm_update(
("did:ng:" + session.private_store_id).substring(0, 53), (this.scope.length == 0
? "did:ng:" + session.private_store_id
: this.scope) as string,
this.shapeType.shape, this.shapeType.shape,
ormPatches, ormPatches,
session.session_id session.session_id
); );
}); });
} };
private onBackendMessage(...message: any) {
this.handleInitialResponse(message);
}
private handleInitialResponse(...param: any) {
console.log("RESPONSE FROM BACKEND", param);
// TODO: This will break, just provisionary. private onBackendMessage = ({ V0: data }: any) => {
const wasmMessage: any = param; if (data.OrmInitial) {
const { initialData } = wasmMessage; this.handleInitialResponse(data.OrmInitial);
}
};
private handleInitialResponse = (initialData: any) => {
// Assign initial data to empty signal object without triggering watcher at first. // Assign initial data to empty signal object without triggering watcher at first.
this.suspendDeepWatcher = true; this.suspendDeepWatcher = true;
batch(() => { batch(() => {
// Do this in case the there was any (incorrect) data added before initialization.
this.signalObject.clear();
// Convert arrays to sets and apply to signalObject (we only have sets but can only transport arrays). // Convert arrays to sets and apply to signalObject (we only have sets but can only transport arrays).
for (const newItem of recurseArrayToSet(initialData)) { for (const newItem of recurseArrayToSet(initialData)) {
this.signalObject.add(newItem); this.signalObject.add(newItem);
} }
console.log("data received", this.signalObject);
}); });
queueMicrotask(() => { queueMicrotask(() => {
@ -171,13 +168,13 @@ export class OrmConnection<T extends BaseType> {
}); });
this.ready = true; this.ready = true;
} };
private onBackendUpdate(...params: any) { private onBackendUpdate = (...params: any) => {
// Apply diff // Apply diff
} };
/** Function to create random subject IRIs for newly created nested objects. */ /** Function to create random subject IRIs for newly created nested objects. */
private generateSubjectIri(path: (string | number)[]): string { private generateSubjectIri = (path: (string | number)[]): string => {
// Generate random string. // Generate random string.
let b = Buffer.alloc(33); let b = Buffer.alloc(33);
crypto.getRandomValues(b); crypto.getRandomValues(b);
@ -192,7 +189,7 @@ export class OrmConnection<T extends BaseType> {
// Else, just generate a random IRI. // Else, just generate a random IRI.
return "did:ng:q:" + randomString; return "did:ng:q:" + randomString;
} }
} };
} }
// //
@ -214,6 +211,8 @@ export function deepPatchesToDiff(patches: DeepPatch[]): Patches {
const recurseArrayToSet = (obj: any): any => { const recurseArrayToSet = (obj: any): any => {
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
return new Set(obj.map(recurseArrayToSet)); return new Set(obj.map(recurseArrayToSet));
} else if (obj && typeof obj === "object" && obj instanceof Map) {
return Object.fromEntries(obj.entries());
} else if (obj && typeof obj === "object") { } else if (obj && typeof obj === "object") {
for (const key of Object.keys(obj)) { for (const key of Object.keys(obj)) {
obj[key] = recurseArrayToSet(obj[key]); obj[key] = recurseArrayToSet(obj[key]);

@ -9,14 +9,16 @@ const useShape = <T extends BaseType>(
shape: ShapeType<T>, shape: ShapeType<T>,
scope: Scope = "" scope: Scope = ""
) => { ) => {
const shapeSignalRef = useRef< const shapeSignalRef = useRef<ReturnType<
ReturnType<typeof createSignalObjectForShape<T>> typeof createSignalObjectForShape<T>
>(createSignalObjectForShape(shape, scope)); > | null>(null);
const [, setTick] = useState(0); const [, setTick] = useState(0);
useEffect(() => { useEffect(() => {
shapeSignalRef.current = createSignalObjectForShape(shape, scope);
const handle = shapeSignalRef.current; const handle = shapeSignalRef.current;
const deepSignalObj = handle.signalObject; const deepSignalObj = handle.signalObject;
const { stopListening } = watch(deepSignalObj, () => { const { stopListening } = watch(deepSignalObj, () => {
// trigger a React re-render when the deep signal updates // trigger a React re-render when the deep signal updates
setTick((t) => t + 1); setTick((t) => t + 1);
@ -31,9 +33,7 @@ const useShape = <T extends BaseType>(
}; };
}, []); }, []);
if ("@id" in shapeSignalRef.current.signalObject) return shapeSignalRef.current?.signalObject;
return shapeSignalRef.current.signalObject;
else return null;
}; };
export default useShape; export default useShape;

@ -130,7 +130,7 @@ INSERT DATA {
// Apply ORM patch: Add name // Apply ORM patch: Add name
let diff = vec![OrmPatch { let diff = vec![OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person1/name".to_string(), path: "/urn:test:person1/name".to_string(),
valType: None, valType: None,
value: Some(json!("Alice")), value: Some(json!("Alice")),
}]; }];
@ -229,7 +229,7 @@ INSERT DATA {
// Apply ORM patch: Remove name // Apply ORM patch: Remove name
let diff = vec![OrmPatch { let diff = vec![OrmPatch {
op: OrmPatchOp::remove, op: OrmPatchOp::remove,
path: "urn:test:person2/name".to_string(), path: "/urn:test:person2/name".to_string(),
valType: None, valType: None,
value: Some(json!("Bob")), value: Some(json!("Bob")),
}]; }];
@ -329,13 +329,13 @@ INSERT DATA {
let diff = vec![ let diff = vec![
// OrmDiffOp { // OrmDiffOp {
// op: OrmDiffOpType::remove, // op: OrmDiffOpType::remove,
// path: "urn:test:person3/name".to_string(), // path: "/urn:test:person3/name".to_string(),
// valType: None, // valType: None,
// value: Some(json!("Charlie")), // value: Some(json!("Charlie")),
// }, // },
OrmPatch { OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person3/name".to_string(), path: "/urn:test:person3/name".to_string(),
valType: None, valType: None,
value: Some(json!("Charles")), value: Some(json!("Charles")),
}, },
@ -443,7 +443,7 @@ INSERT DATA {
let diff = vec![OrmPatch { let diff = vec![OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
valType: Some(OrmPatchType::set), valType: Some(OrmPatchType::set),
path: "urn:test:person4/hobby".to_string(), path: "/urn:test:person4/hobby".to_string(),
value: Some(json!("Swimming")), value: Some(json!("Swimming")),
}]; }];
@ -543,7 +543,7 @@ INSERT DATA {
// Apply ORM patch: Remove hobby // Apply ORM patch: Remove hobby
let diff = vec![OrmPatch { let diff = vec![OrmPatch {
op: OrmPatchOp::remove, op: OrmPatchOp::remove,
path: "urn:test:person5/hobby".to_string(), path: "/urn:test:person5/hobby".to_string(),
valType: None, valType: None,
value: Some(json!("Swimming")), value: Some(json!("Swimming")),
}]; }];
@ -712,7 +712,7 @@ INSERT DATA {
// Apply ORM patch: Change city in nested address // Apply ORM patch: Change city in nested address
let diff = vec![OrmPatch { let diff = vec![OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person6/address/city".to_string(), path: "/urn:test:person6/address/city".to_string(),
valType: None, valType: None,
value: Some(json!("Shelbyville")), value: Some(json!("Shelbyville")),
}]; }];
@ -931,7 +931,7 @@ INSERT DATA {
// Apply ORM patch: Change street in company's headquarter address (3 levels deep) // Apply ORM patch: Change street in company's headquarter address (3 levels deep)
let diff = vec![OrmPatch { let diff = vec![OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person7/company/urn:test:company1/headquarter/street".to_string(), path: "/urn:test:person7/company/urn:test:company1/headquarter/street".to_string(),
valType: None, valType: None,
value: Some(json!("Rich Street")), value: Some(json!("Rich Street")),
}]; }];
@ -1038,7 +1038,7 @@ INSERT DATA {
let diff = vec![ let diff = vec![
OrmPatch { OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person8".to_string(), path: "/urn:test:person8".to_string(),
valType: Some(OrmPatchType::object), valType: Some(OrmPatchType::object),
value: None, value: None,
}, },
@ -1046,19 +1046,19 @@ INSERT DATA {
// This does nothing as it does not represent a triple. // This does nothing as it does not represent a triple.
// A subject is created when inserting data. // A subject is created when inserting data.
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person8/@id".to_string(), path: "/urn:test:person8/@id".to_string(),
valType: Some(OrmPatchType::object), valType: Some(OrmPatchType::object),
value: None, value: None,
}, },
OrmPatch { OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person8/type".to_string(), path: "/urn:test:person8/type".to_string(),
valType: None, valType: None,
value: Some(json!("http://example.org/Person")), value: Some(json!("http://example.org/Person")),
}, },
OrmPatch { OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person8/name".to_string(), path: "/urn:test:person8/name".to_string(),
valType: None, valType: None,
value: Some(json!("Alice")), value: Some(json!("Alice")),
}, },
@ -1218,13 +1218,13 @@ INSERT DATA {
let diff = vec![ let diff = vec![
OrmPatch { OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person9/address/http:~1~1example.org~1exampleAddress/type".to_string(), path: "/urn:test:person9/address/http:~1~1example.org~1exampleAddress/type".to_string(),
valType: None, valType: None,
value: Some(json!("http://example.org/Address")), value: Some(json!("http://example.org/Address")),
}, },
OrmPatch { OrmPatch {
op: OrmPatchOp::add, op: OrmPatchOp::add,
path: "urn:test:person9/address/http:~1~1example.org~1exampleAddress/street" path: "/urn:test:person9/address/http:~1~1example.org~1exampleAddress/street"
.to_string(), .to_string(),
valType: None, valType: None,
value: Some(json!("Heaven Avenue")), value: Some(json!("Heaven Avenue")),

Loading…
Cancel
Save