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) =
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.
if revert_inserts.len() > 0 || revert_removes.len() > 0 {
let revert_changes = GraphQuadsPatch {
inserts: revert_removes,
removes: revert_inserts,
};
log_info!("[orm_frontend_update] Reverting");
// TODO: Call with correct params.
// self.orm_backend_update(session_id, scope, "", revert_changes)
@ -63,7 +66,7 @@ impl Verifier {
diff: OrmPatches,
) -> Result<(), String> {
log_info!(
"frontend_update_orm session={} shape={} diff={:?}",
"[orm_frontend_update] session={} shape={} diff={:?}",
session_id,
shape_iri,
diff
@ -74,12 +77,17 @@ impl Verifier {
self.get_first_orm_subscription_for(scope, Some(&shape_iri), Some(&session_id));
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);
log_info!(
"[orm_frontend_update] created sparql_update query:\n{}",
sparql_update
);
(doc_nuri, sparql_update)
};
log_debug!("Created SPARQL query for patches:\n{}", sparql_update);
match self
.process_sparql_update(
&doc_nuri,
@ -90,8 +98,17 @@ impl Verifier {
)
.await
{
Err(e) => Err(e),
Err(e) => {
log_info!("[orm_frontend_update] query failed");
Err(e)
}
Ok((_, revert_inserts, revert_removes, skolemnized_blank_nodes)) => {
log_info!(
"[orm_frontend_update] query successful. Reverts? {}",
revert_inserts.len()
);
if !revert_inserts.is_empty()
|| !revert_removes.is_empty()
|| !skolemnized_blank_nodes.is_empty()
@ -117,6 +134,11 @@ fn create_sparql_update_query_for_diff(
orm_subscription: &OrmSubscription,
diff: OrmPatches,
) -> String {
log_info!(
"[create_sparql_update_query_for_diff] Starting with {} patches",
diff.len()
);
// First sort patches.
// - Process delete patches first.
// - 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()
.filter(|patch| patch.op == OrmPatchOp::remove)
.collect();
log_info!(
"[create_sparql_update_query_for_diff] Found {} delete patches",
delete_patches.len()
);
let add_object_patches: Vec<_> = diff
.iter()
.filter(|patch| {
@ -134,7 +161,12 @@ fn create_sparql_update_query_for_diff(
}
})
.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()
.filter(|patch| {
patch.op == OrmPatchOp::add
@ -144,19 +176,33 @@ fn create_sparql_update_query_for_diff(
}
})
.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.
let mut sparql_sub_queries: Vec<String> = vec![];
// 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 (where_statements, target, _pred_schema) =
create_where_statements_for_patch(&del_patch, &mut var_counter, &orm_subscription);
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;
if let Some(target_object) = 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,
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)
//
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:
// 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.
// 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;
// Create WHERE statements from path.
// POTENTIAL PANIC SOURCE: create_where_statements_for_patch can panic in several places
let (where_statements, target, pred_schema) =
create_where_statements_for_patch(&add_patch, &mut var_counter, &orm_subscription);
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 {
// 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?
continue;
} 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 ", ").
None => {
// 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?
continue;
}
@ -225,6 +290,7 @@ fn create_sparql_update_query_for_diff(
// If the schema only has max one value,
// then `add` can also overwrite values, so we need to delete the previous one
if !pred_schema.unwrap().is_multi() {
log_info!("[create_sparql_update_query_for_diff] Single-value predicate, adding DELETE before INSERT");
let remove_statement =
format!(" {} <{}> ?o{}", subject_var, target_predicate, var_counter);
@ -236,6 +302,10 @@ fn create_sparql_update_query_for_diff(
remove_statement,
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.
}
// The actual INSERT.
@ -245,9 +315,17 @@ fn create_sparql_update_query_for_diff(
add_statement,
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");
}
@ -294,6 +372,12 @@ fn create_where_statements_for_patch(
(String, String, Option<String>),
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 where_statements: Vec<String> = vec![];
@ -303,9 +387,20 @@ fn create_where_statements_for_patch(
.map(|s| decode_json_pointer(&s.to_string()))
.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.
if path.len() == 1 {
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));
where_statements.push(format!("<{}> ?p ?o", root_iri));
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
.shape_type
.schema
.get(&orm_subscription.shape_type.shape)
.unwrap();
log_info!("[create_where_statements_for_patch] Root schema found");
let mut current_subj_schema: Arc<OrmSchemaShape> = subj_schema.clone();
// The root IRI might change, if the parent path segment was an IRI.
let root_iri = path.remove(0);
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 {
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);
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.
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 (
where_statements,
(subject_ref, pred_schema.iri.clone(), None),
@ -346,6 +473,12 @@ fn create_where_statements_for_patch(
"{} <{}> ?o{}",
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);
subject_ref = format!("?o{}", var_counter);
@ -358,9 +491,19 @@ fn create_where_statements_for_patch(
);
}
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);
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.
if path.len() == 0 {
log_info!(
"[create_where_statements_for_patch] Path ends on object IRI. Returning target with object={}",
object_iri
);
return (
where_statements,
(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 =
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.
subject_ref = format!("<{object_iri}>");
// And can clear all, now unnecessary where statements.
where_statements.clear();
log_info!(
"[create_where_statements_for_patch] Reset subject_ref to <{}> and cleared where statements",
object_iri
);
} else {
// Set to child subject schema.
// 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.
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);
log_info!("[create_where_statements_for_patch] Child schema found");
}
}
// Can't happen.
log_err!("[create_where_statements_for_patch] PANIC: Reached end of function unexpectedly (should be impossible)");
panic!();
}

@ -35,15 +35,14 @@ impl Verifier {
// &update.overlay_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()))
.map_err(|e| NgError::OxiGraphError(e.to_string()))?;
let parsed =
Query::parse(&query, nuri_str).map_err(|e| NgError::OxiGraphError(e.to_string()))?;
let results = oxistore
.query(parsed, Some(nuri.to_string()))
.query(parsed, nuri)
.map_err(|e| NgError::OxiGraphError(e.to_string()))?;
match results {
QueryResults::Graph(triples) => {
@ -51,8 +50,7 @@ impl Verifier {
for t in triples {
match t {
Err(e) => {
log_info!("Error: {:?}n", e);
log_err!("{}", e.to_string());
return Err(NgError::SparqlError(e.to_string()));
}
Ok(triple) => {

@ -15,11 +15,9 @@ use std::sync::Arc;
use futures::channel::mpsc;
use futures::SinkExt;
use futures::StreamExt;
use ng_net::actor::SoS;
use ng_net::types::InboxPost;
use ng_net::types::NgQRCode;
use ng_net::types::NgQRCodeProfileSharingV0;
use ng_oxigraph::oxigraph::sparql::EvaluationError;
use ng_oxigraph::oxigraph::sparql::{results::*, Query, QueryResults};
use ng_oxigraph::oxrdf::{Literal, NamedNode, Quad, Term};
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. */
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. */
root: symbol;
/** 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
| DeepLiteralAddPatch
);
export interface DeepSetAddPatch {
/** Mutation kind applied at the resolved `path`. */
op: "add";
@ -105,7 +116,7 @@ function buildPath(
return path;
}
function queuePatch(patch: DeepPatch) {
function queuePatch(patch: DeepPatchInternal) {
if (!pendingPatches) pendingPatches = new Map();
const root = patch.root;
let list = pendingPatches.get(root);
@ -113,6 +124,9 @@ function queuePatch(patch: DeepPatch) {
list = [];
pendingPatches.set(root, list);
}
// Remove root, we do not send that back.
// @ts-ignore
delete patch.root;
list.push(patch);
if (!microtaskScheduled) {
microtaskScheduled = true;
@ -124,7 +138,7 @@ function queuePatch(patch: DeepPatch) {
for (const [rootId, patches] of groups) {
if (!patches.length) continue;
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 { deepSignal, DeepPatch, DeepSignalOptions } from "../deepSignal";
import {
deepSignal,
DeepPatchInternal,
DeepSignalOptions,
} from "../deepSignal";
import { watch } from "../watch";
describe("deepSignal options", () => {
@ -12,7 +16,7 @@ describe("deepSignal options", () => {
};
const state = deepSignal({ data: {} as any }, options);
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
@ -51,7 +55,7 @@ describe("deepSignal options", () => {
};
const state = deepSignal({ s: new Set<any>() }, options);
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
@ -76,7 +80,7 @@ describe("deepSignal options", () => {
};
const state = deepSignal({ root: {} as any }, options);
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
@ -119,7 +123,7 @@ describe("deepSignal options", () => {
};
const state = deepSignal({ items: [] as any[] }, options);
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
@ -148,7 +152,7 @@ describe("deepSignal options", () => {
};
const state = deepSignal({ s: new Set<any>() }, options);
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
@ -210,7 +214,7 @@ describe("deepSignal options", () => {
};
const state = deepSignal({ container: {} as any }, options);
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
@ -283,7 +287,7 @@ describe("deepSignal options", () => {
describe("backward compatibility", () => {
it("still works without options", async () => {
const state = deepSignal({ data: { value: 1 } });
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
@ -298,7 +302,7 @@ describe("deepSignal options", () => {
// TODO: Delete duplicate logic for `id`. Only accept @id.
it("objects with id property still work for Sets", async () => {
const state = deepSignal({ s: new Set<any>() });
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
@ -315,7 +319,7 @@ describe("deepSignal options", () => {
it("@id takes precedence over id property", async () => {
const state = deepSignal({ s: new Set<any>() });
const patches: DeepPatch[][] = [];
const patches: DeepPatchInternal[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);

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

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

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

@ -21,7 +21,7 @@
}
const flatEntries = $derived(
$shapeObject
? flattenObject($shapeObject.entries().next() || ({} as any))
? $shapeObject.entries().map((o) => flattenObject(o)[0] || ({} as any))
: []
);
$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 ng_net::orm::OrmPatch;
use ng_repo::log_info;
use ng_wallet::types::SensitiveWallet;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
@ -1314,7 +1315,9 @@ async fn app_request_stream_(
// 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 {
// let response: Object = response_js.try_into().map_err(|_| {
// "Error while adding triples to AppResponse.V0.State".to_string()
@ -1818,7 +1821,6 @@ pub async fn orm_start(
) -> Result<JsValue, String> {
let shape_type: OrmShapeType = serde_wasm_bindgen::from_value::<OrmShapeType>(shapeType)
.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)
.map_err(|_| "Deserialization error of session_id".to_string())?;
let scope = if scope.is_empty() {
@ -1826,6 +1828,7 @@ pub async fn orm_start(
} else {
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);
request.set_session_id(session_id);
app_request_stream_(request, callback).await
@ -1833,21 +1836,24 @@ pub async fn orm_start(
#[wasm_bindgen]
pub async fn orm_update(
scope: JsValue,
scope: String,
shapeTypeName: String,
diff: JsValue,
session_id: JsValue,
) -> Result<(), String> {
let diff: OrmPatches = serde_wasm_bindgen::from_value::<OrmPatches>(diff)
.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)
.map_err(|_| "Deserialization error of scope".to_string())?;
let scope = if scope.is_empty() {
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 session_id: u64 = serde_wasm_bindgen::from_value::<u64>(session_id)
.map_err(|_| "Deserialization error of session_id".to_string())?;
request.set_session_id(session_id);
log_info!("[orm_update] calling orm_update");
let response = nextgraph::local_broker::app_request(request)
.await
.map_err(|e: NgError| e.to_string())?;

@ -72,14 +72,10 @@ export class OrmConnection<T extends BaseType> {
ngSession.then(({ ng, session }) => {
console.log("ng and session", ng, session);
try {
const sc = ("did:ng:" + session.private_store_id).substring(
0,
53
);
console.log("calling orm_start with nuri", sc);
ng.orm_start(
sc,
(scope.length == 0
? "did:ng:" + session.private_store_id
: scope) as string,
shapeType,
session.session_id,
this.onBackendMessage
@ -97,10 +93,10 @@ export class OrmConnection<T extends BaseType> {
* @param ng
* @returns
*/
public static getConnection<T extends BaseType>(
public static getConnection = <T extends BaseType>(
shapeType: ShapeType<T>,
scope: Scope
): OrmConnection<T> {
): OrmConnection<T> => {
const scopeKey = canonicalScope(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);
return newConnection;
}
}
};
public release() {
public release = () => {
if (this.refCount > 0) this.refCount--;
if (this.refCount === 0) {
OrmConnection.idToEntry.delete(this.identifier);
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;
const ormPatches = deepPatchesToDiff(patches);
ngSession.then(({ ng, session }) => {
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,
ormPatches,
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.
const wasmMessage: any = param;
const { initialData } = wasmMessage;
private onBackendMessage = ({ V0: data }: any) => {
if (data.OrmInitial) {
this.handleInitialResponse(data.OrmInitial);
}
};
private handleInitialResponse = (initialData: any) => {
// Assign initial data to empty signal object without triggering watcher at first.
this.suspendDeepWatcher = true;
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).
for (const newItem of recurseArrayToSet(initialData)) {
this.signalObject.add(newItem);
}
console.log("data received", this.signalObject);
});
queueMicrotask(() => {
@ -171,13 +168,13 @@ export class OrmConnection<T extends BaseType> {
});
this.ready = true;
}
private onBackendUpdate(...params: any) {
};
private onBackendUpdate = (...params: any) => {
// Apply diff
}
};
/** 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.
let b = Buffer.alloc(33);
crypto.getRandomValues(b);
@ -192,7 +189,7 @@ export class OrmConnection<T extends BaseType> {
// Else, just generate a random IRI.
return "did:ng:q:" + randomString;
}
}
};
}
//
@ -214,6 +211,8 @@ export function deepPatchesToDiff(patches: DeepPatch[]): Patches {
const recurseArrayToSet = (obj: any): any => {
if (Array.isArray(obj)) {
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") {
for (const key of Object.keys(obj)) {
obj[key] = recurseArrayToSet(obj[key]);

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

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

Loading…
Cancel
Save