diff --git a/src/frontends/react/HelloWorld.tsx b/src/frontends/react/HelloWorld.tsx index f70b3a6..9d3fc96 100644 --- a/src/frontends/react/HelloWorld.tsx +++ b/src/frontends/react/HelloWorld.tsx @@ -1,22 +1,25 @@ import React from "react"; -import { useSignal } from "@gn8/alien-signals-react"; - -import { - deepSignalExample, - computedSignalFromExample, -} from "src/ng-mock/js-land/signalLibs/alienSignals"; +import useShape from "../../ng-mock/js-land/frontendAdapters/react/useShape"; export function HelloWorldReact() { - const [state] = useSignal(deepSignalExample); - const [computed] = useSignal(computedSignalFromExample); + const state = useShape("Shape2", ""); + + window.reactState = state; + console.log("react render", state); + + if (!state) return <>Loading state; return (

Hello World from React!

-

Here is a reactive object with {computed.get()} props.

-
{JSON.stringify(state, null, 4)}
-
); diff --git a/src/frontends/svelte/HelloWorld.svelte b/src/frontends/svelte/HelloWorld.svelte index 75bcf44..539b749 100644 --- a/src/frontends/svelte/HelloWorld.svelte +++ b/src/frontends/svelte/HelloWorld.svelte @@ -1,24 +1,19 @@

Hello World from Svelte!

-

Here is a reactive object with {$computed.get()} props.

{JSON.stringify($nestedObject, null, 4)}
diff --git a/src/frontends/vue/HelloWorld.vue b/src/frontends/vue/HelloWorld.vue index 362ef46..c1c3fc9 100644 --- a/src/frontends/vue/HelloWorld.vue +++ b/src/frontends/vue/HelloWorld.vue @@ -1,22 +1,29 @@ \ No newline at end of file diff --git a/src/ng-mock/js-land/connector/applyDiff.ts b/src/ng-mock/js-land/connector/applyDiff.ts index 4f49540..f5fb856 100644 --- a/src/ng-mock/js-land/connector/applyDiff.ts +++ b/src/ng-mock/js-land/connector/applyDiff.ts @@ -1,6 +1,9 @@ import type { Diff } from "../types"; /** Mock function to apply diffs. Just uses a copy of the diff as the new object. */ -export function applyDiff(currentState: object, diff: Diff): object { - return JSON.parse(JSON.stringify(diff)); +export function applyDiff(currentState: any, diff: Diff) { + const clone = JSON.parse(JSON.stringify(diff)); + Object.keys(clone).forEach((k) => { + currentState[k] = clone[k]; + }); } diff --git a/src/ng-mock/js-land/connector/ngSignals.ts b/src/ng-mock/js-land/connector/ngSignals.ts index 8f437c0..5a8aa8a 100644 --- a/src/ng-mock/js-land/connector/ngSignals.ts +++ b/src/ng-mock/js-land/connector/ngSignals.ts @@ -1,31 +1,70 @@ -import handleShapeUpdate from "src/ng-mock/wasm-land/handleShapeUpdate"; +import updateShape from "src/ng-mock/wasm-land/updateShape"; import type { Connection, Diff, Scope, Shape } from "../types"; -import handleShapeRequest from "src/ng-mock/wasm-land/handleShapeRequest"; +import requestShape from "src/ng-mock/wasm-land/requestShape"; import { applyDiff } from "./applyDiff"; +import { batch, deepSignal, watch } from "alien-deepsignals"; +import { signal } from "alien-signals"; -export async function createSignalObjectForShape(shape: Shape, scope?: Scope) { - const ret: { - state?: any; - connectionId?: Connection["id"]; - update: (diff: Diff) => Promise; - } = { - async update(diff) { - if (!ret.connectionId) - throw new Error("Connection not established yet for shape" + shape); - await handleShapeUpdate(ret.connectionId, diff); - }, +// TODO: The code is horrible. +export function createSignalObjectForShape(shape: Shape, scope?: Scope) { + // TODO: + // DeepSignal has a different API to alien-signals. + // Therefore, we need to create a "root signal" wrapper that is + // triggered on deepSignal changes. + const rootSignal = signal(null); + + // State + let stopWatcher: any = null; + let suspendWatcher = false; + + // To update the root signal + const setUpDeepSignal = ( + newSignal: ReturnType, + connectionId: Connection["id"] + ) => { + stopWatcher?.(); + + // Notify DB on changes. + stopWatcher = watch( + newSignal, + (newVal, oldVal, onCleanup) => { + if (!suspendWatcher) updateShape(connectionId, newVal); + }, + { deep: true } + ); + + // Update the root signal. + rootSignal(newSignal); }; - const onDbUpdate = (diff: Diff) => { - ret.state = applyDiff(ret.state || {}, diff); + const onUpdateFromDb = (diff: Diff, connectionId: Connection["id"]) => { + const nestedObj = rootSignal(); + console.log("Update received", connectionId, diff); + // Set new value from applying the diffs to the old value. + + // suspendWatcher = true; + // We need to replace the root signal for now, so this is redundant. + // batch(() => { + // if (!nestedObj) return; // This shouldn't happen but we make the compiler happy. + // applyDiff(nestedObj, diff); + // }); + // suspendWatcher = false;nestedObj + + // Create a new deep signal. + // We need to do that because the deepSignals ref hasn't changed otherwise + // and no update is triggered. + const newDeepSignal = deepSignal(JSON.parse(JSON.stringify(diff))); + setUpDeepSignal(newDeepSignal, connectionId); }; - await handleShapeRequest(shape, onDbUpdate).then( + // Do the actual db request. + requestShape(shape, scope, onUpdateFromDb).then( ({ connectionId, shapeObject }) => { - ret.state = shapeObject; - ret.connectionId = connectionId; + // Create a deepSignal to put into the vanilla alien-signal. + const deepSignalFromDb = deepSignal(shapeObject); + setUpDeepSignal(deepSignalFromDb, connectionId); } ); - return ret; + return rootSignal; } diff --git a/src/ng-mock/js-land/frontendAdapters/react/useShape.ts b/src/ng-mock/js-land/frontendAdapters/react/useShape.ts new file mode 100644 index 0000000..84975af --- /dev/null +++ b/src/ng-mock/js-land/frontendAdapters/react/useShape.ts @@ -0,0 +1,19 @@ +import { useSignal } from "@gn8/alien-signals-react"; +import { useMemo } from "react"; +import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/ngSignals"; +import type { Scope, Shape } from "src/ng-mock/js-land/types"; + +const useShape = (shape: Shape, scope: Scope) => { + const signalOfShape = useMemo(() => { + console.log("react memo called..."); + return createSignalObjectForShape(shape, scope); + }, [shape, scope]); + + const [shapeObject, setShapeObject] = useSignal(signalOfShape); + + // We don't need the setter. + // The object is recursively proxied and changes are recorded there. + return shapeObject; +}; + +export default useShape; diff --git a/src/ng-mock/js-land/frontendAdapters/svelte/useShape.ts b/src/ng-mock/js-land/frontendAdapters/svelte/useShape.ts new file mode 100644 index 0000000..3f93157 --- /dev/null +++ b/src/ng-mock/js-land/frontendAdapters/svelte/useShape.ts @@ -0,0 +1,13 @@ +import { useSignal } from "@gn8/alien-signals-svelte"; +import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/ngSignals"; +import type { Scope, Shape } from "src/ng-mock/js-land/types"; + +const useShape = (shape: Shape, scope: Scope) => { + const signalOfShape = createSignalObjectForShape(shape, scope); + + const writeableStoreForShape = useSignal(signalOfShape); + + return writeableStoreForShape; +}; + +export default useShape; diff --git a/src/ng-mock/js-land/frontendAdapters/vue/useShape.ts b/src/ng-mock/js-land/frontendAdapters/vue/useShape.ts new file mode 100644 index 0000000..e5eacbe --- /dev/null +++ b/src/ng-mock/js-land/frontendAdapters/vue/useShape.ts @@ -0,0 +1,13 @@ +import { useSignal } from "@gn8/alien-signals-vue"; +import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/ngSignals"; +import type { Scope, Shape } from "src/ng-mock/js-land/types"; + +const useShape = (shape: Shape, scope: Scope) => { + const signalOfShape = createSignalObjectForShape(shape, scope); + + const refOfShape = useSignal(signalOfShape); + + return refOfShape; +}; + +export default useShape; diff --git a/src/ng-mock/js-land/signalLibs/alienSignals.ts b/src/ng-mock/js-land/signalLibs/alienSignals.ts deleted file mode 100644 index 3175291..0000000 --- a/src/ng-mock/js-land/signalLibs/alienSignals.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { deepSignal, watch, computed } from "alien-deepsignals"; -import { signal } from "alien-signals"; - -const deepSignalExample = deepSignal({ - count: 0, - name: "John", - nested: { - deep: "value", - }, - array: [1, 2, 3], - set: new Set(["el1"]), -}); - -const computedSignalFromExample = computed(() => { - return Object.keys(deepSignalExample).length; -}); - -/** Create a vanilla alien-signals Signal so that it can be normally used by universe-alien-signals. */ -const wrapDeepIntoAlienSignal = ( - nestedSignal: ReturnType> -) => { - const alienSignal = signal(nestedSignal); - - // Watch deep signal and notify vanilla alien-signal on changes. - watch( - nestedSignal, - (value) => { - // We need to destructure because the object otherwise remained the same. - alienSignal({ ...nestedSignal }); - }, - { deep: true } - ); - - return alienSignal; -}; - -const wrappedDeepSignalState = wrapDeepIntoAlienSignal(deepSignalExample); -const wrappedComputedPropertiesOfObj = wrapDeepIntoAlienSignal( - computedSignalFromExample -); - -export { - wrappedDeepSignalState as deepSignalExample, - wrappedComputedPropertiesOfObj as computedSignalFromExample, -}; diff --git a/src/ng-mock/tests/subscriptionToStore.test.ts b/src/ng-mock/tests/subscriptionToStore.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/ng-mock/tests/updatesWithWasm.test.ts b/src/ng-mock/tests/updatesWithWasm.test.ts index 65abdd9..686b40b 100644 --- a/src/ng-mock/tests/updatesWithWasm.test.ts +++ b/src/ng-mock/tests/updatesWithWasm.test.ts @@ -1,23 +1,30 @@ import { expect, test } from "vitest"; import { createSignalObjectForShape } from "../js-land/connector/ngSignals"; +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +// TODO: Redo test("shape object notification comes back to others", async () => { - const object1 = await createSignalObjectForShape("Shape1"); - const object2 = await createSignalObjectForShape("Shape1"); + const object1 = createSignalObjectForShape("Shape1"); + const object2 = createSignalObjectForShape("Shape1"); + + const object3 = createSignalObjectForShape("Shape2"); + const object4 = createSignalObjectForShape("Shape2"); - const object3 = await createSignalObjectForShape("Shape2"); - const object4 = await createSignalObjectForShape("Shape2"); + wait(50); // Update object 1 and expect object 2 to update as well. - await object1.update({ name: "Updated name from object1" }); + object1()!.name = "Updated name from object1"; - expect(object2.state?.name).toBe("Updated name from object1"); + wait(30); + expect(object2()!.name).toBe("Updated name from object1"); // Expect object of different shape not to have changed. - expect(object3.state?.name).toBe("Niko's cat"); + expect(object3().name).toBe("Niko's cat"); // Update object 4 and expect object 3 with same shape to have updated. - await object4.update({ name: "Updated name from object4" }); + object4()!.name = "Updated name from object4"; - expect(object3.state?.name).toBe("Updated name from object4"); + wait(30); + expect(object3()!.name).toBe("Updated name from object4"); }); diff --git a/src/ng-mock/wasm-land/handleShapeRequest.ts b/src/ng-mock/wasm-land/requestShape.ts similarity index 82% rename from src/ng-mock/wasm-land/handleShapeRequest.ts rename to src/ng-mock/wasm-land/requestShape.ts index 2530e3a..10e31af 100644 --- a/src/ng-mock/wasm-land/handleShapeRequest.ts +++ b/src/ng-mock/wasm-land/requestShape.ts @@ -1,6 +1,5 @@ -import { randomUUID } from "crypto"; import * as shapeManager from "./shapeManager"; -import type { WasmConnection, Diff } from "./types"; +import type { WasmConnection, Diff, Scope } from "./types"; import type { Shape } from "../js-land/types"; const mockShapeObject1 = { @@ -20,19 +19,21 @@ const mockShapeObject2 = { numberOfHomes: 3, address: { street: "Niko's street", - compartment: 2, + houseNumber: "15", + floor: 0, }, }; -export default async function handleShapeRequest( +export default async function requestShape( shape: Shape, + scope: Scope | undefined, callback: (diff: Diff, connectionId: WasmConnection["id"]) => void ): Promise<{ connectionId: string; shapeObject: object; }> { const connection: WasmConnection = { - id: randomUUID(), + id: Math.random().toFixed(4), shape, // Create a deep copy to prevent accidental by-reference changes. state: JSON.parse( diff --git a/src/ng-mock/wasm-land/handleShapeUpdate.ts b/src/ng-mock/wasm-land/updateShape.ts similarity index 82% rename from src/ng-mock/wasm-land/handleShapeUpdate.ts rename to src/ng-mock/wasm-land/updateShape.ts index 4caad93..c713667 100644 --- a/src/ng-mock/wasm-land/handleShapeUpdate.ts +++ b/src/ng-mock/wasm-land/updateShape.ts @@ -1,13 +1,15 @@ import * as shapeManager from "./shapeManager"; import type { WasmConnection, Diff } from "./types"; -export default async function handleShapeUpdate( +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;