From 2dc6136d5109dda7ef3ce51762ab5f11cf822978 Mon Sep 17 00:00:00 2001 From: Laurin Weger Date: Sat, 11 Oct 2025 15:10:02 +0200 Subject: [PATCH] rm obsolete ng-mock --- .../multi-framework-signals/package.json | 6 +- .../src/frontends/react/HelloWorld.tsx | 3 - .../src/ng-mock/tests/applyDiff.test.ts | 217 ---------------- .../src/ng-mock/tests/updatesWithWasm.test.ts | 56 ----- .../wasm-land/objectBuilder/buildObjects.ts | 147 ----------- .../src/ng-mock/wasm-land/shapeHandler.ts | 172 ------------- .../src/ng-mock/wasm-land/shapeManager.ts | 10 - .../wasm-land/sparql/buildConstruct.ts | 220 ---------------- .../sparql/buildSparqlConstructFromShape.ts | 140 ----------- .../wasm-land/sparql/sparqlConstruct.test.ts | 9 - .../wasm-land/sparql/testShape.schema.ts | 129 ---------- .../src/ng-mock/wasm-land/types.ts | 18 -- .../src/ng-mock/wasm-land/updateShape.ts | 22 -- .../src/connector/applyDiff.test.ts | 235 ++++++++++++++++++ 14 files changed, 236 insertions(+), 1148 deletions(-) delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/applyDiff.test.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/updatesWithWasm.test.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/objectBuilder/buildObjects.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeManager.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/sparqlConstruct.test.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/types.ts delete mode 100644 sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/updateShape.ts create mode 100644 sdk/ng-sdk-js/ng-signals/src/connector/applyDiff.test.ts diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/package.json b/sdk/ng-sdk-js/examples/multi-framework-signals/package.json index 6733498..df0c511 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/package.json +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/package.json @@ -21,17 +21,13 @@ "vite-plugin-top-level-await": "^1.6.0", "@astrojs/svelte": "7.1.0", "@astrojs/vue": "^5.1.0", - "@gn8/alien-signals-react": "^0.1.1", - "@gn8/alien-signals-solid": "^0.1.1", - "@gn8/alien-signals-svelte": "^0.1.1", - "@gn8/alien-signals-vue": "^0.1.1", "@nextgraph-monorepo/ng-alien-deepsignals": "workspace:*", "@nextgraph-monorepo/ng-shex-orm": "workspace:*", "@types/react": "19.1.10", "@types/react-dom": "19.1.7", "@types/shexj": "^2.1.7", "alien-signals": "^2.0.7", - "astro": "5.13.2", + "astro": "5.14.4", "install": "^0.13.0", "npm": "^11.5.2", "prettier-eslint": "^16.4.2", diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx b/sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx index b4b04fa..41161a1 100644 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx +++ b/sdk/ng-sdk-js/examples/multi-framework-signals/src/frontends/react/HelloWorld.tsx @@ -3,9 +3,6 @@ import { useShape } from "@nextgraph-monorepo/ng-signals/react"; import flattenObject from "../utils/flattenObject"; import { TestObjectShapeType } from "../../shapes/ldo/testShape.shapeTypes"; -// Hack to get mock backend started -import { mockTestObject } from "../../ng-mock/wasm-land/shapeHandler"; - export function HelloWorldReact() { const state = useShape(TestObjectShapeType); diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/applyDiff.test.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/applyDiff.test.ts deleted file mode 100644 index a18a496..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/applyDiff.test.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { describe, test, expect } from "vitest"; -import { - applyDiff, -} from "@nextgraph-monorepo/ng-signals"; -import type { Patch } from "@nextgraph-monorepo/ng-signals"; - -/** - * Build a patch path string from segments (auto-prefix /) - */ -function p(...segs: (string | number)[]) { - return "/" + segs.map(String).join("/"); -} - -describe("applyDiff - set operations (primitives)", () => { - test("add single primitive into new set", () => { - const state: any = {}; - const diff: Patch[] = [ - { op: "add", type: "set", path: p("tags"), value: "a" }, - ]; - applyDiff(state, diff); - expect(state.tags).toBeInstanceOf(Set); - expect([...state.tags]).toEqual(["a"]); - }); - test("add multiple primitives into new set", () => { - const state: any = {}; - const diff: Patch[] = [ - { op: "add", type: "set", path: p("nums"), value: [1, 2, 3] }, - ]; - applyDiff(state, diff); - expect([...state.nums]).toEqual([1, 2, 3]); - }); - test("add primitives merging into existing set", () => { - const state: any = { nums: new Set([1]) }; - const diff: Patch[] = [ - { op: "add", type: "set", path: p("nums"), value: [2, 3] }, - ]; - applyDiff(state, diff); - expect([...state.nums].sort()).toEqual([1, 2, 3]); - }); - test("remove single primitive from set", () => { - const state: any = { tags: new Set(["a", "b"]) }; - const diff: Patch[] = [ - { op: "remove", type: "set", path: p("tags"), value: "a" }, - ]; - applyDiff(state, diff); - expect([...state.tags]).toEqual(["b"]); - }); - test("remove multiple primitives from set", () => { - const state: any = { nums: new Set([1, 2, 3, 4]) }; - const diff: Patch[] = [ - { op: "remove", type: "set", path: p("nums"), value: [2, 4] }, - ]; - applyDiff(state, diff); - expect([...state.nums].sort()).toEqual([1, 3]); - }); -}); - -describe("applyDiff - set operations (object sets)", () => { - test("add object entries to new object-set", () => { - const state: any = {}; - const diff: Patch[] = [ - { - op: "add", - type: "set", - path: p("users"), - value: { u1: { id: "u1", n: 1 }, u2: { id: "u2", n: 2 } }, - }, - ]; - applyDiff(state, diff); - expect(state.users.u1).toEqual({ id: "u1", n: 1 }); - expect(state.users.u2).toEqual({ id: "u2", n: 2 }); - }); - test("merge object entries into existing object-set", () => { - const state: any = { users: { u1: { id: "u1", n: 1 } } }; - const diff: Patch[] = [ - { - op: "add", - type: "set", - path: p("users"), - value: { u2: { id: "u2", n: 2 } }, - }, - ]; - applyDiff(state, diff); - expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]); - }); - test("remove object entries from object-set", () => { - const state: any = { users: { u1: {}, u2: {}, u3: {} } }; - const diff: Patch[] = [ - { op: "remove", type: "set", path: p("users"), value: ["u1", "u3"] }, - ]; - applyDiff(state, diff); - expect(Object.keys(state.users)).toEqual(["u2"]); - }); - test("adding primitives to existing object-set replaces with Set", () => { - const state: any = { mixed: { a: {}, b: {} } }; - const diff: Patch[] = [ - { op: "add", type: "set", path: p("mixed"), value: [1, 2] }, - ]; - applyDiff(state, diff); - expect(state.mixed).toBeInstanceOf(Set); - expect([...state.mixed]).toEqual([1, 2]); - }); -}); - -describe("applyDiff - object & literal operations", () => { - test("add object (create empty object)", () => { - const state: any = {}; - const diff: Patch[] = [{ op: "add", path: p("address"), type: "object" }]; - applyDiff(state, diff); - expect(state.address).toEqual({}); - }); - test("add nested object path with ensurePathExists", () => { - const state: any = {}; - const diff: Patch[] = [ - { op: "add", path: p("a", "b", "c"), type: "object" }, - ]; - applyDiff(state, diff, true); - expect(state.a.b.c).toEqual({}); - }); - test("add primitive value", () => { - const state: any = { address: {} }; - const diff: Patch[] = [ - { op: "add", path: p("address", "street"), value: "1st" }, - ]; - applyDiff(state, diff); - expect(state.address.street).toBe("1st"); - }); - test("overwrite primitive value", () => { - const state: any = { address: { street: "old" } }; - const diff: Patch[] = [ - { op: "add", path: p("address", "street"), value: "new" }, - ]; - applyDiff(state, diff); - expect(state.address.street).toBe("new"); - }); - test("remove primitive", () => { - const state: any = { address: { street: "1st", country: "Greece" } }; - const diff: Patch[] = [{ op: "remove", path: p("address", "street") }]; - applyDiff(state, diff); - expect(state.address.street).toBeUndefined(); - expect(state.address.country).toBe("Greece"); - }); - test("remove object branch", () => { - const state: any = { address: { street: "1st" }, other: 1 }; - const diff: Patch[] = [{ op: "remove", path: p("address") }]; - applyDiff(state, diff); - expect(state.address).toBeUndefined(); - expect(state.other).toBe(1); - }); -}); - -describe("applyDiff - multiple mixed patches in a single diff", () => { - test("sequence of mixed set/object/literal add & remove", () => { - const state: any = { - users: { u1: { id: "u1" } }, - tags: new Set(["old"]), - }; - const diff: Patch[] = [ - { op: "add", type: "set", path: p("users"), value: { u2: { id: "u2" } } }, - { op: "add", path: p("profile"), type: "object" }, - { op: "add", path: p("profile", "name"), value: "Alice" }, - { op: "add", type: "set", path: p("tags"), value: ["new"] }, - { op: "remove", type: "set", path: p("tags"), value: "old" }, - ]; - applyDiff(state, diff); - expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]); - expect(state.profile.name).toBe("Alice"); - expect([...state.tags]).toEqual(["new"]); - }); - - test("complex nested path creation and mutations with ensurePathExists", () => { - const state: any = {}; - const diff: Patch[] = [ - { op: "add", path: p("a", "b"), type: "object" }, - { op: "add", path: p("a", "b", "c"), value: 1 }, - { op: "add", type: "set", path: p("a", "nums"), value: [1, 2, 3] }, - { op: "remove", type: "set", path: p("a", "nums"), value: 2 }, - { op: "add", path: p("a", "b", "d"), value: 2 }, - { op: "remove", path: p("a", "b", "c") }, - ]; - applyDiff(state, diff, true); - expect(state.a.b.c).toBeUndefined(); - expect(state.a.b.d).toBe(2); - expect(state.a.nums).toBeInstanceOf(Set); - expect([...state.a.nums].sort()).toEqual([1, 3]); - }); -}); - -describe("applyDiff - ignored / invalid scenarios", () => { - test("skip patch with non-leading slash path", () => { - const state: any = {}; - const diff: Patch[] = [{ op: "add", path: "address/street", value: "x" }]; - applyDiff(state, diff); - expect(state).toEqual({}); - }); - test("missing parent without ensurePathExists -> patch skipped and no mutation", () => { - const state: any = {}; - const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }]; - applyDiff(state, diff, false); - expect(state).toEqual({}); - }); -}); - -describe("applyDiff - ignored / invalid scenarios", () => { - test("skip patch with non-leading slash path", () => { - const state: any = {}; - const diff: Patch[] = [{ op: "add", path: "address/street", value: "x" }]; - applyDiff(state, diff); - expect(state).toEqual({}); - }); - test("missing parent without ensurePathExists -> patch skipped and no mutation", () => { - const state: any = {}; - const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }]; - applyDiff(state, diff, false); - expect(state).toEqual({}); - }); -}); diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/updatesWithWasm.test.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/updatesWithWasm.test.ts deleted file mode 100644 index 5d96a24..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/tests/updatesWithWasm.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, test } from "vitest"; -import { createSignalObjectForShape } from "@nextgraph-monorepo/ng-signals"; - -const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -describe("Signal modification and propagation to backend with or without signal pooling", () => { - for (const withPooling of [true, false]) { - test(`shape object notification comes back to others ${ - withPooling ? "with" : "without" - } signal pooling`, async () => { - const object1 = createSignalObjectForShape( - "TestShape", - undefined, - withPooling - ); - const object2 = createSignalObjectForShape( - "TestShape", - undefined, - withPooling - ); - - const object3 = createSignalObjectForShape( - "Shape2", - undefined, - withPooling - ); - const object4 = createSignalObjectForShape( - "Shape2", - undefined, - withPooling - ); - - await wait(10); - - // Update object 1 and expect object 2 to update as well. - // @ts-expect-error - object1.name = "Updated name from object1"; - - await wait(10); - // @ts-expect-error - expect(object2.name).toBe("Updated name from object1"); - - // Expect object of different shape not to have changed. - // @ts-expect-error - expect(object3.name).toBe("Niko's cat"); - - // Update object 4 and expect object 3 with same shape to have updated. - // @ts-expect-error - object4.name = "Updated name from object4"; - - await wait(10); - // @ts-expect-error - expect(object3!.name).toBe("Updated name from object4"); - }); - } -}); diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/objectBuilder/buildObjects.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/objectBuilder/buildObjects.ts deleted file mode 100644 index 0aac1dd..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/objectBuilder/buildObjects.ts +++ /dev/null @@ -1,147 +0,0 @@ -import type { - ShapeType, - Shape, - Predicate, - Schema, -} from "@nextgraph-monorepo/ng-shex-orm"; - -interface Triple { - s: string; - p: string; - o: string; -} -interface TrackedSubject { - shape: Shape; - parentObject?: TrackedSubject; - valid?: boolean; - trackedPredicates: Record; - /** For Sub-objects only */ - untracked?: boolean; -} - -interface TrackedPredicate { - shape: Predicate; - currentCardinality: number; - childSubjects?: TrackedSubject[]; -} - -export function buildObjects(shapeType: ShapeType) { - // -} - -export function onTriplesRemoved( - trackedSubjects: Record, - triplesRemoved: string[][], - shapeType: ShapeType -) { - // -} - -/** - * Adds new triples to tracked subjects and creates and returns - * new tracked (possibly nested) subjects. - */ -export function onTriplesAdded( - trackedSubjects: Record, - triplesAdded: Triple[], - schema: Schema, - rootShape?: Shape -): { newTrackedSubjects: Record } { - // Track for secondary iterations. - const newTrackedSubjectsCreated: Record = {}; - - for (const triple of triplesAdded) { - if (!trackedSubjects[triple.s]) { - // If rootShape is not set, we are applying the triples new nested objects - // and we don't create new tracked subjects. - if (!rootShape) continue; - - // Check if predicate is in root shape type. - const matchedPredicate = rootShape.predicates.find((p) => triple.p); - if (!matchedPredicate) { - // Nothing to track. - continue; - } - // The triple should be tracked. Create new tracked subject. - const newTrackedSubject: TrackedSubject = { - shape: rootShape, - trackedPredicates: { - [triple.p]: { - shape: matchedPredicate, - currentCardinality: 1, - }, - }, - }; - trackedSubjects[triple.s] = [newTrackedSubject]; - } else { - // Add triple to tracked subject(s). - // In the case of nested shapes, the same subject can be tracked - // in multiple shapes. - const trackedSubjectsMatching = trackedSubjects[triple.s]; - for (const trackedSubject of trackedSubjectsMatching) { - // Is predicate tracked in this subject's shape? - const matchedPredShape = trackedSubject.shape.predicates.find( - (predShape) => predShape.iri === triple.p - ); - if (!matchedPredShape) continue; - // Get or create tracked predicate for tracked shape. - let trackedPredicate = - trackedSubject.trackedPredicates[matchedPredShape?.iri]; - if (!trackedSubjects) { - trackedPredicate = { - currentCardinality: 0, - shape: matchedPredShape, - }; - } - // Increase cardinality. - trackedPredicate.currentCardinality += 1; - - // If predicate shape has nested object, track that too. - if (trackedPredicate.shape.nestedShape) { - const newTrackedObject: TrackedSubject = { - shape: schema[matchedPredShape.nestedShape as string], - trackedPredicates: {}, - parentObject: trackedSubject, - }; - // Remember for adding to registry and for re-running on nested shapes. - const newTracked = newTrackedSubjectsCreated[triple.o]; - if (!newTracked) - newTrackedSubjectsCreated[triple.o] = [ - newTrackedObject, - ]; - newTracked.push(newTrackedObject); - - // Link to parent - const childSubjects = trackedPredicate.childSubjects; - if (childSubjects) childSubjects.push(newTrackedObject); - else trackedPredicate.childSubjects = [newTrackedObject]; - } - - // TODO: Inform tracked subjects about change - } - } - } - - // Rerun triples on new tracked subjects created. - // Then merge with parent tracked subjects - const newNestedSubjectsCreated = onTriplesAdded( - newTrackedSubjectsCreated, - triplesAdded, - schema - ); - - // TODO: Is it correct to do this? - const ret: Record = {}; - for (const subjectIri of Object.keys(newTrackedSubjectsCreated)) { - ret[subjectIri] = [ - ...newTrackedSubjectsCreated[subjectIri], - ...newNestedSubjectsCreated.newTrackedSubjects[subjectIri], - ]; - } - - // Update Valid invalid - - return { newTrackedSubjects: ret }; -} - -// Do we have infinite-loop issues? diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts deleted file mode 100644 index 5b4f267..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeHandler.ts +++ /dev/null @@ -1,172 +0,0 @@ -import * as shapeManager from "./shapeManager"; -import type { WasmConnection, Diff, Scope } from "./types"; -import type { ShapeType, BaseType } from "@nextgraph-monorepo/ng-shex-orm"; -import * as ng from "@nextgraph-monorepo/ng-sdk-js"; -import type { Person } from "../../shapes/ldo/personShape.typings"; -import type { Cat } from "../../shapes/ldo/catShape.typings"; -import type { TestObject } from "../../shapes/ldo/testShape.typings"; -import updateShape from "./updateShape"; -import { testShapeSchema } from "../../shapes/ldo/testShape.schema"; -import { TestObjectShapeType } from "../../shapes/ldo/testShape.shapeTypes"; - -// Messages exchanged over the BroadcastChannel("shape-manager") -interface WasmMessage { - type: - | "Request" - | "InitialResponse" - | "FrontendUpdate" - | "BackendUpdate" - | "Stop"; - connectionId: string; - diff?: Diff; - shapeType?: ShapeType; - initialData?: BaseType; -} - -export const mockTestObject = { - id: "ex:mock-id-1", - type: "TestObject", - stringValue: "string", - numValue: 42, - boolValue: true, - arrayValue: [1, 2, 3], - objectValue: { - id: "urn:obj-1", - nestedString: "nested", - nestedNum: 7, - - nestedArray: [10, 12], - }, - anotherObject: { - "id:1": { - id: "id:1", - prop1: "prop1 value", - prop2: 100, - }, - "id:2": { - id: "id:1", - prop1: "prop2 value", - prop2: 200, - }, - }, -} satisfies TestObject; - -const mockShapeObject1 = { - id: "ex:person-1", - type: "Person", - name: "Bob", - address: { - id: "urn:person-home-1", - street: "First street", - houseNumber: "15", - }, - hasChildren: true, - numberOfHouses: 0, -} satisfies Person; - -const mockShapeObject2 = { - id: "ex:cat-1", - type: "Cat", - name: "Niko's cat", - age: 12, - numberOfHomes: 3, - address: { - id: "Nikos-cat-home", - street: "Niko's street", - houseNumber: "15", - floor: 0, - }, -} satisfies Cat; - -// Single BroadcastChannel for wasm-land side -const communicationChannel = new BroadcastChannel("shape-manager"); - -function getInitialObjectByShapeId(shapeId?: string): T { - if (shapeId?.includes("TestObject")) return mockTestObject as unknown as T; - if (shapeId?.includes("Person")) return mockShapeObject1 as unknown as T; - if (shapeId?.includes("Cat")) return mockShapeObject2 as unknown as T; - console.warn( - "BACKEND: requestShape for unknown shape, returning empty object.", - shapeId - ); - return {} as T; -} - -// Register handler for messages coming from js-land -communicationChannel.addEventListener( - "message", - (event: MessageEvent) => { - console.log("BACKEND: Received message", event.data); - // call WASM ng-sdk-js - const { type, connectionId, shapeType } = event.data; - - if (type === "Request") { - ng.orm_update("", "", {}, 1); - /* unsub = await ng.orm_start(scope, shapeType, session_id, - async (response) => { - //console.log("GOT APP RESPONSE", response); - if (response.V0.OrmInitial) { - - } else if (response.V0.OrmUpdate) { - let diff = response.V0.OrmUpdate.diff - const msg: WasmMessage = { - type: "BackendUpdate", - connectionId, - diff, - }; - communicationChannel.postMessage(msg); - } else if (response.V0.OrmError) { - - } - */ - - const shapeId = shapeType?.shape; - const initialData = getInitialObjectByShapeId(shapeId); - - // Store connection. We store the shapeId string to allow equality across connections. - shapeManager.connections.set(connectionId, { - id: connectionId, - // Cast to any to satisfy WasmConnection type, comparison in updateShape uses == - shape: (shapeId ?? "__unknown__") as any, - state: initialData, - callback: (diff: Diff, conId: WasmConnection["id"]) => { - // Notify js-land about backend updates - const msg: WasmMessage = { - type: "BackendUpdate", - connectionId, - diff, - }; - communicationChannel.postMessage(msg); - }, - }); - - const msg: WasmMessage = { - type: "InitialResponse", - connectionId, - initialData, - }; - communicationChannel.postMessage(msg); - return; - } - - if (type === "Stop") { - shapeManager.connections.delete(connectionId); - // await ng.app_request ( OrmStop ) - return; - } - - if (type === "FrontendUpdate" && event.data.diff) { - updateShape(connectionId, event.data.diff); - return; - } - - console.warn( - "BACKEND: Unknown message type or missing diff", - event.data - ); - } -); - -ng.orm_start("", TestObjectShapeType, 12, (...params: any) => { - console.log("updates received with params", params); -}).catch((err) => console.error(err)); diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeManager.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeManager.ts deleted file mode 100644 index 5807530..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/shapeManager.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Diff, ObjectState, WasmConnection } from "./types"; - -const connections: Map = new Map(); - -/** Mock function to apply diffs. Just uses a copy of the diff as the new object. */ -export function applyDiff(currentState: ObjectState, diff: Diff): ObjectState { - return JSON.parse(JSON.stringify(diff)); -} - -export { connections }; diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts deleted file mode 100644 index 34a1f0b..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildConstruct.ts +++ /dev/null @@ -1,220 +0,0 @@ -import type { Predicate, Shape, Schema } from "@nextgraph-monorepo/ng-shex-orm"; - -/** - * Build a SPARQL CONSTRUCT query from a ShapeConstraint definition. - * The WHERE mirrors the graph template. Optional predicates (min=0) are wrapped in OPTIONAL in WHERE - * but still appear in the CONSTRUCT template so that matched triples are constructed. - */ -export function buildConstructQuery( - shape: Shape, - schema: Schema, - options?: SparqlBuildOptions -): string { - const ctx: BuildContext = { usedVars: new Set() }; - const prefixes = prefixesToText(options?.prefixes); - const subject = shape.iri; - - const templateLines: string[] = []; - const whereLines: string[] = []; - const postFilters: string[] = []; - const valuesBlocks: string[] = []; - - const rootVar = - subject.startsWith("?") || subject.startsWith("$") - ? subject - : uniqueVar(ctx, "s"); - if (!subject.startsWith("?") && !subject.startsWith("$")) { - valuesBlocks.push(valuesBlock(rootVar, [subject] as any)); - } - - const predicates = Array.isArray(shape.predicates) - ? shape.predicates - : [...shape.predicates]; - for (const pred of predicates) { - addConstructPattern( - ctx, - pred, - rootVar, - templateLines, - whereLines, - postFilters, - valuesBlocks, - options - ); - } - - const graphWrap = (body: string) => - options?.graph - ? `GRAPH ${toIriOrCurie(options.graph)} {\n${body}\n}` - : body; - - const where = [ - ...valuesBlocks, - graphWrap(whereLines.join("\n")), - ...postFilters, - ] - .filter(Boolean) - .join("\n"); - - const template = templateLines.join("\n"); - - return [prefixes, `CONSTRUCT {`, template, `} WHERE {`, where, `}`].join( - "\n" - ); -} - -function addConstructPattern( - ctx: BuildContext, - pred: Predicate, - subjectVar: string, - template: string[], - where: string[], - postFilters: string[], - valuesBlocks: string[], - options?: SparqlBuildOptions -) { - const p = `<${pred.iri}>`; - const objVar = uniqueVar(ctx, pred.readablePredicate); - - const triple = `${subjectVar} ${p} ${objTerm} .`; - - const isOptional = - (pred.minCardinality ?? 0) === 0 && - (options?.includeOptionalForMinZero ?? true); - - if (pred.dataTypes === "nested" && pred.nestedShape) { - template.push(triple); - const nestedBody: string[] = [triple]; - const nestedPreds = pred.nestedShape.predicates; - - for (const n of nestedPreds) { - addConstructPattern( - ctx, - n, - objTerm, - template, - nestedBody, - postFilters, - valuesBlocks, - options - ); - } - const block = nestedBody.join("\n"); - where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block); - return; - } - - // Non-nested - template.push(triple); - const blockLines: string[] = [triple]; - - if (pred.dataTypes === "literal" && pred.literalValue !== undefined) { - if (Array.isArray(pred.literalValue)) { - valuesBlocks.push(valuesBlock(objVar, pred.literalValue as any[])); - } else { - const lit = - typeof pred.literalValue === "string" || - typeof pred.literalValue === "number" || - typeof pred.literalValue === "boolean" - ? pred.literalValue - : String(pred.literalValue); - postFilters.push( - `FILTER(${objVar} = ${typeof lit === "string" ? `"${String(lit).replace(/"/g, '\\"')}"` : lit})` - ); - } - } - - const block = blockLines.join("\n"); - where.push(isOptional ? `OPTIONAL {\n${block}\n}` : block); -} - -export type LiteralKind = - | "number" - | "string" - | "boolean" - | "nested" - | "literal"; - -export interface SparqlBuildOptions { - prefixes?: Record; - graph?: string; // IRI of the named graph to query, if any - includeOptionalForMinZero?: boolean; // default true -} - -export const defaultPrefixes: Record = { - xsd: "http://www.w3.org/2001/XMLSchema#", - rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - rdfs: "http://www.w3.org/2000/01/rdf-schema#", -}; - -export function prefixesToText(prefixes?: Record): string { - const all = { ...defaultPrefixes, ...(prefixes ?? {}) }; - return Object.entries(all) - .map(([p, iri]) => `PREFIX ${p}: <${iri}>`) - .join("\n"); -} - -export function safeVarName(name: string): string { - const base = name - .replace(/[^a-zA-Z0-9_]/g, "_") - .replace(/^([0-9])/, "_$1") - .slice(0, 60); - return base || "v"; -} - -export function varToken(name: string): string { - const n = - name.startsWith("?") || name.startsWith("$") ? name.slice(1) : name; - return `?${safeVarName(n)}`; -} - -export function formatLiteral(value: string | number | boolean): string { - if (typeof value === "number") return String(value); - if (typeof value === "boolean") return value ? "true" : "false"; - // default string literal - const escaped = value.replace(/"/g, '\\"'); - return `"${escaped}"`; -} - -export function formatTermForValues(value: string | number | boolean): string { - if (typeof value === "number" || typeof value === "boolean") - return formatLiteral(value); - // strings: detect IRI or CURIE and keep raw; otherwise quote - const v = value.trim(); - const looksLikeIri = v.startsWith("<") && v.endsWith(">"); - const looksLikeHttp = v.includes("://"); - const looksLikeCurie = - /^[A-Za-z_][A-Za-z0-9_-]*:.+$/u.test(v) && !looksLikeHttp; - if (looksLikeIri || looksLikeHttp || looksLikeCurie) { - return looksLikeHttp ? `<${v}>` : v; - } - return formatLiteral(v); -} - -export function valuesBlock( - varName: string, - values: Array -): string { - const rendered = values.map(formatTermForValues).join(" "); - return `VALUES ${varName} { ${rendered} }`; -} - -export interface BuildContext { - // Tracks used variable names to avoid collisions - usedVars: Set; -} - -export function uniqueVar(ctx: BuildContext, base: string): string { - let candidate = varToken(base); - if (!ctx.usedVars.has(candidate)) { - ctx.usedVars.add(candidate); - return candidate; - } - let i = 2; - while (ctx.usedVars.has(`${candidate}_${i}`)) i++; - const unique = `${candidate}_${i}`; - ctx.usedVars.add(unique); - return unique; -} - -export default buildConstructQuery; diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts deleted file mode 100644 index 98e932d..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/buildSparqlConstructFromShape.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type { Predicate, Shape, Schema } from "@nextgraph-monorepo/ng-shex-orm"; - -export const buildConstructQuery = ({ - schema, - shapeId, -}: { - schema: Schema; - shapeId: keyof Schema; -}): string => { - const rootShape = schema[shapeId]; - - const constructStatements: { - s: string; - p: string; - o: string; - optional: boolean; - literals: Predicate["literalValue"]; - }[] = []; - - const idToVarName: Record = {}; - const getVarNameFor = (id: string) => { - const currentName = idToVarName[id]; - if (currentName) return currentName; - - const newVar = `o${Object.entries(idToVarName).length + 1}`; - idToVarName[id] = newVar; - return newVar; - }; - - // Create s,p,o records where subject and object var names are mapped to shape or predicate ids. - const addTriples = (shape: Shape) => { - const predicates = shape.predicates; - const shapeId = shape.iri; - - for (const pred of predicates) { - const subjectVarName = getVarNameFor(shapeId); - - if (pred.dataTypes === "nested") { - if (typeof pred.nestedShape !== "string") - throw new Error("Nested shapes must be by reference"); - - // If a name for this shape was assigned already, it's triples have been added - // and we don't have to recurse. - const shapeAlreadyRegistered = !!idToVarName[pred.nestedShape]; - - const shapeVarName = getVarNameFor(pred.nestedShape); - - constructStatements.push({ - s: `?${subjectVarName}`, - p: `<${pred.iri}>`, - o: `?${shapeVarName}`, - optional: pred.minCardinality < 1, - literals: pred.literalValue, - // TODO: eitherOf ? - }); - - if (!shapeAlreadyRegistered) - addTriples(schema[pred.nestedShape]); - } else { - const objVarName = getVarNameFor( - shapeId + "__separator__" + pred.iri - ); - - constructStatements.push({ - s: `?${subjectVarName}`, - p: `<${pred.iri}>`, - o: `?${objVarName}`, - optional: pred.minCardinality < 1, - literals: pred.literalValue, - // TODO: eitherOf ? - }); - } - } - }; - - addTriples(rootShape); - - const construct = `CONSTRUCT { -${constructStatements.map(({ s, p, o }) => ` ${s} ${p} ${o} .\n`).join("")} }`; - - const statementToWhere = ({ - s, - p, - o, - optional, - }: { - s: string; - p: string; - o: string; - optional: boolean; - }) => { - if (optional) return ` OPTIONAL { ${s} ${p} ${o} . }\n`; - else return ` ${s} ${p} ${o} .\n`; - }; - - const literalToSparqlFormat = ( - literal: string | number | boolean - ): string => { - if (typeof literal === "number") return String(literal); - if (typeof literal === "boolean") return literal ? "true" : "false"; - if (typeof literal === "string") { - return isIri(literal) - ? `<${literal}>` - : `"${escapeString(literal)}"`; - } - return `"${String(literal)}"`; - }; - - // Filters for optional values. - const filters = constructStatements - .filter((statement) => statement.literals !== undefined) - .map((statement) => { - const vals = arrayOf(statement.literals!); - if (vals.length === 0) return ""; - if (vals.length === 1) { - return ` FILTER(${statement.o} = ${literalToSparqlFormat(vals[0]!)})\n`; - } - const list = vals.map(literalToSparqlFormat).join(", "); - return ` FILTER(${statement.o} IN (${list}))\n`; - }) - .join(""); - - const where = `WHERE { - ${constructStatements.map(statementToWhere).join("")} - ${filters} - }`; - - return `${construct}\n${where}`; -}; - -const arrayOf = (arrayOrLiteral: T | T[]) => { - if (typeof arrayOrLiteral === "undefined" || arrayOrLiteral === null) - return []; - if (Array.isArray(arrayOrLiteral)) return arrayOrLiteral; - return [arrayOrLiteral]; -}; - -const isIri = (str: string) => /^[a-zA-Z][a-zA-Z0-9+.-]{1,7}:/.test(str); - -const escapeString = (str: string) => str.replace(/["\\]/g, "\\$&"); diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/sparqlConstruct.test.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/sparqlConstruct.test.ts deleted file mode 100644 index c32421e..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/sparqlConstruct.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { buildConstructQuery } from "./buildSparqlConstructFromShape.ts"; -import { testShapeSchema } from "./testShape.schema.ts"; - -console.log( - buildConstructQuery({ - schema: testShapeSchema, - shapeId: "http://example.org/TestObject", - }) -); diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts deleted file mode 100644 index ab13bb9..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/sparql/testShape.schema.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { Schema } from "@nextgraph-monorepo/ng-shex-orm"; - -/** - * ============================================================================= - * testShapeSchema: Schema for testShape - * ============================================================================= - */ -export const testShapeSchema: Schema = { - "http://example.org/TestObject": { - iri: "http://example.org/TestObject", - predicates: [ - { - dataTypes: "literal", - literalValue: ["TestObject"], - maxCardinality: 1, - minCardinality: 1, - iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", - readablePredicate: "type", - extra: true, - }, - { - dataTypes: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/stringValue", - readablePredicate: "stringValue", - }, - { - dataTypes: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/numValue", - readablePredicate: "numValue", - }, - { - dataTypes: "boolean", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/boolValue", - readablePredicate: "boolValue", - }, - { - dataTypes: "number", - maxCardinality: -1, - minCardinality: 0, - iri: "http://example.org/arrayValue", - readablePredicate: "arrayValue", - }, - { - dataTypes: "nested", - nestedShape: - "http://example.org/TestObject||http://example.org/objectValue", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/objectValue", - readablePredicate: "objectValue", - }, - { - dataTypes: "nested", - nestedShape: - "http://example.org/TestObject||http://example.org/anotherObject", - maxCardinality: -1, - minCardinality: 0, - iri: "http://example.org/anotherObject", - readablePredicate: "anotherObject", - }, - { - dataTypes: "eitherOf", - eitherOf: [ - { - valType: "string", - }, - { - valType: "number", - }, - ], - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/numOrStr", - readablePredicate: "numOrStr", - }, - ], - }, - "http://example.org/TestObject||http://example.org/objectValue": { - iri: "http://example.org/TestObject||http://example.org/objectValue", - predicates: [ - { - dataTypes: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/nestedString", - readablePredicate: "nestedString", - }, - { - dataTypes: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/nestedNum", - readablePredicate: "nestedNum", - }, - { - dataTypes: "number", - maxCardinality: -1, - minCardinality: 0, - iri: "http://example.org/nestedArray", - readablePredicate: "nestedArray", - }, - ], - }, - "http://example.org/TestObject||http://example.org/anotherObject": { - iri: "http://example.org/TestObject||http://example.org/anotherObject", - predicates: [ - { - dataTypes: "string", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/prop1", - readablePredicate: "prop1", - }, - { - dataTypes: "number", - maxCardinality: 1, - minCardinality: 1, - iri: "http://example.org/prop2", - readablePredicate: "prop2", - }, - ], - }, -}; diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/types.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/types.ts deleted file mode 100644 index a3b3da8..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { ShapeType, BaseType } from "@nextgraph-monorepo/ng-shex-orm"; -import type { Patch } from "@nextgraph-monorepo/ng-signals"; - -/** The Scope of a shape request */ -export type Scope = string | string[]; - -/** The diff format used to communicate updates between wasm-land and js-land. */ -export type Diff = Patch[]; - -export type ObjectState = object; - -/** A connection established between wasm-land and js-land for subscription of a shape. */ -export type WasmConnection = { - id: string; - shape: ShapeType; - state: ObjectState; - callback: (diff: Diff, connectionId: WasmConnection["id"]) => void; -}; diff --git a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/updateShape.ts b/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/updateShape.ts deleted file mode 100644 index 4ea1a6f..0000000 --- a/sdk/ng-sdk-js/examples/multi-framework-signals/src/ng-mock/wasm-land/updateShape.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as shapeManager from "./shapeManager"; -import type { WasmConnection, Diff } from "./types"; - -export default async function updateShape( - connectionId: WasmConnection["id"], - diff: Diff, -) { - const connection = shapeManager.connections.get(connectionId); - if (!connection) throw new Error("No Connection found."); - - console.log("BACKEND: Received update request from ", connectionId); - - const newState = shapeManager.applyDiff(connection.state, diff); - connection.state = newState; - - shapeManager.connections.forEach((con) => { - // if (con.shape == connection.shape) { - // con.state = newState; - // con.callback(diff, con.id); - // } - }); -} diff --git a/sdk/ng-sdk-js/ng-signals/src/connector/applyDiff.test.ts b/sdk/ng-sdk-js/ng-signals/src/connector/applyDiff.test.ts new file mode 100644 index 0000000..a575df4 --- /dev/null +++ b/sdk/ng-sdk-js/ng-signals/src/connector/applyDiff.test.ts @@ -0,0 +1,235 @@ +import { describe, test, expect } from "vitest"; +import { applyDiff, Patch } from "../index.ts"; + +/** + * Build a patch path string from segments (auto-prefix /) + */ +function p(...segs: (string | number)[]) { + return "/" + segs.map(String).join("/"); +} + +describe("applyDiff - set operations (primitives)", () => { + test("add single primitive into new set", () => { + const state: any = {}; + const diff: Patch[] = [ + { op: "add", valType: "set", path: p("tags"), value: "a" }, + ]; + applyDiff(state, diff); + expect(state.tags).toBeInstanceOf(Set); + expect([...state.tags]).toEqual(["a"]); + }); + test("add multiple primitives into new set", () => { + const state: any = {}; + const diff: Patch[] = [ + { op: "add", valType: "set", path: p("nums"), value: [1, 2, 3] }, + ]; + applyDiff(state, diff); + expect([...state.nums]).toEqual([1, 2, 3]); + }); + test("add primitives merging into existing set", () => { + const state: any = { nums: new Set([1]) }; + const diff: Patch[] = [ + { op: "add", valType: "set", path: p("nums"), value: [2, 3] }, + ]; + applyDiff(state, diff); + expect([...state.nums].sort()).toEqual([1, 2, 3]); + }); + test("remove single primitive from set", () => { + const state: any = { tags: new Set(["a", "b"]) }; + const diff: Patch[] = [ + { op: "remove", valType: "set", path: p("tags"), value: "a" }, + ]; + applyDiff(state, diff); + expect([...state.tags]).toEqual(["b"]); + }); + test("remove multiple primitives from set", () => { + const state: any = { nums: new Set([1, 2, 3, 4]) }; + const diff: Patch[] = [ + { op: "remove", valType: "set", path: p("nums"), value: [2, 4] }, + ]; + applyDiff(state, diff); + expect([...state.nums].sort()).toEqual([1, 3]); + }); +}); + +describe("applyDiff - set operations (object sets)", () => { + test("add object entries to new object-set", () => { + const state: any = {}; + const diff: Patch[] = [ + { + op: "add", + valType: "set", + path: p("users"), + value: { u1: { id: "u1", n: 1 }, u2: { id: "u2", n: 2 } }, + }, + ]; + applyDiff(state, diff); + expect(state.users.u1).toEqual({ id: "u1", n: 1 }); + expect(state.users.u2).toEqual({ id: "u2", n: 2 }); + }); + test("merge object entries into existing object-set", () => { + const state: any = { users: { u1: { id: "u1", n: 1 } } }; + const diff: Patch[] = [ + { + op: "add", + valType: "set", + path: p("users"), + value: { u2: { id: "u2", n: 2 } }, + }, + ]; + applyDiff(state, diff); + expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]); + }); + test("remove object entries from object-set", () => { + const state: any = { users: { u1: {}, u2: {}, u3: {} } }; + const diff: Patch[] = [ + { + op: "remove", + valType: "set", + path: p("users"), + value: ["u1", "u3"], + }, + ]; + applyDiff(state, diff); + expect(Object.keys(state.users)).toEqual(["u2"]); + }); + test("adding primitives to existing object-set replaces with Set", () => { + const state: any = { mixed: { a: {}, b: {} } }; + const diff: Patch[] = [ + { op: "add", valType: "set", path: p("mixed"), value: [1, 2] }, + ]; + applyDiff(state, diff); + expect(state.mixed).toBeInstanceOf(Set); + expect([...state.mixed]).toEqual([1, 2]); + }); +}); + +describe("applyDiff - object & literal operations", () => { + test("add object (create empty object)", () => { + const state: any = {}; + const diff: Patch[] = [ + { op: "add", path: p("address"), valType: "object" }, + ]; + applyDiff(state, diff); + expect(state.address).toEqual({}); + }); + test("add nested object path with ensurePathExists", () => { + const state: any = {}; + const diff: Patch[] = [ + { op: "add", path: p("a", "b", "c"), valType: "object" }, + ]; + applyDiff(state, diff, true); + expect(state.a.b.c).toEqual({}); + }); + test("add primitive value", () => { + const state: any = { address: {} }; + const diff: Patch[] = [ + { op: "add", path: p("address", "street"), value: "1st" }, + ]; + applyDiff(state, diff); + expect(state.address.street).toBe("1st"); + }); + test("overwrite primitive value", () => { + const state: any = { address: { street: "old" } }; + const diff: Patch[] = [ + { op: "add", path: p("address", "street"), value: "new" }, + ]; + applyDiff(state, diff); + expect(state.address.street).toBe("new"); + }); + test("remove primitive", () => { + const state: any = { address: { street: "1st", country: "Greece" } }; + const diff: Patch[] = [{ op: "remove", path: p("address", "street") }]; + applyDiff(state, diff); + expect(state.address.street).toBeUndefined(); + expect(state.address.country).toBe("Greece"); + }); + test("remove object branch", () => { + const state: any = { address: { street: "1st" }, other: 1 }; + const diff: Patch[] = [{ op: "remove", path: p("address") }]; + applyDiff(state, diff); + expect(state.address).toBeUndefined(); + expect(state.other).toBe(1); + }); +}); + +describe("applyDiff - multiple mixed patches in a single diff", () => { + test("sequence of mixed set/object/literal add & remove", () => { + const state: any = { + users: { u1: { id: "u1" } }, + tags: new Set(["old"]), + }; + const diff: Patch[] = [ + { + op: "add", + valType: "set", + path: p("users"), + value: { u2: { id: "u2" } }, + }, + { op: "add", path: p("profile"), valType: "object" }, + { op: "add", path: p("profile", "name"), value: "Alice" }, + { op: "add", valType: "set", path: p("tags"), value: ["new"] }, + { op: "remove", valType: "set", path: p("tags"), value: "old" }, + ]; + applyDiff(state, diff); + expect(Object.keys(state.users).sort()).toEqual(["u1", "u2"]); + expect(state.profile.name).toBe("Alice"); + expect([...state.tags]).toEqual(["new"]); + }); + + test("complex nested path creation and mutations with ensurePathExists", () => { + const state: any = {}; + const diff: Patch[] = [ + { op: "add", path: p("a", "b"), valType: "object" }, + { op: "add", path: p("a", "b", "c"), value: 1 }, + { + op: "add", + valType: "set", + path: p("a", "nums"), + value: [1, 2, 3], + }, + { op: "remove", valType: "set", path: p("a", "nums"), value: 2 }, + { op: "add", path: p("a", "b", "d"), value: 2 }, + { op: "remove", path: p("a", "b", "c") }, + ]; + applyDiff(state, diff, true); + expect(state.a.b.c).toBeUndefined(); + expect(state.a.b.d).toBe(2); + expect(state.a.nums).toBeInstanceOf(Set); + expect([...state.a.nums].sort()).toEqual([1, 3]); + }); +}); + +describe("applyDiff - ignored / invalid scenarios", () => { + test("skip patch with non-leading slash path", () => { + const state: any = {}; + const diff: Patch[] = [ + { op: "add", path: "address/street", value: "x" }, + ]; + applyDiff(state, diff); + expect(state).toEqual({}); + }); + test("missing parent without ensurePathExists -> patch skipped and no mutation", () => { + const state: any = {}; + const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }]; + applyDiff(state, diff, false); + expect(state).toEqual({}); + }); +}); + +describe("applyDiff - ignored / invalid scenarios", () => { + test("skip patch with non-leading slash path", () => { + const state: any = {}; + const diff: Patch[] = [ + { op: "add", path: "address/street", value: "x" }, + ]; + applyDiff(state, diff); + expect(state).toEqual({}); + }); + test("missing parent without ensurePathExists -> patch skipped and no mutation", () => { + const state: any = {}; + const diff: Patch[] = [{ op: "add", path: p("a", "b", "c"), value: 1 }]; + applyDiff(state, diff, false); + expect(state).toEqual({}); + }); +});