From 63646f3e1add49804f77d61e3305027f134026ba Mon Sep 17 00:00:00 2001 From: Laurin Weger Date: Sat, 23 Aug 2025 17:47:51 +0200 Subject: [PATCH] chore: do not create new deepSignal on every update --- .../connector/createSignalObjectForShape.ts | 84 +++++++++++++++++++ src/ng-mock/js-land/connector/ngSignals.ts | 75 ----------------- .../frontendAdapters/react/useShape.ts | 9 +- .../frontendAdapters/svelte/useShape.ts | 7 +- .../js-land/frontendAdapters/vue/useShape.ts | 7 +- src/ng-mock/tests/updatesWithWasm.test.ts | 2 +- src/ng-mock/wasm-land/requestShape.ts | 4 +- 7 files changed, 103 insertions(+), 85 deletions(-) create mode 100644 src/ng-mock/js-land/connector/createSignalObjectForShape.ts delete mode 100644 src/ng-mock/js-land/connector/ngSignals.ts diff --git a/src/ng-mock/js-land/connector/createSignalObjectForShape.ts b/src/ng-mock/js-land/connector/createSignalObjectForShape.ts new file mode 100644 index 0000000..f2a8fb5 --- /dev/null +++ b/src/ng-mock/js-land/connector/createSignalObjectForShape.ts @@ -0,0 +1,84 @@ +import updateShape from "src/ng-mock/wasm-land/updateShape"; +import type { Connection, Diff, Scope, Shape } from "../types"; +import requestShape from "src/ng-mock/wasm-land/requestShape"; +import { applyDiff } from "./applyDiff"; +import { batch, deepSignal, watch } from "alien-deepsignals"; +import { signal as createSignal, computed } from "alien-signals"; + +type ReactiveShapeObject = object; + +type ShapeObjectSignal = ReturnType< + typeof createSignal<{ + content: ReactiveShapeObject | null; + }> +>; + +const openConnections: Partial> = {}; + +/** + * Create a signal for a shape object. + * The function returns a signal of a shape object in the form: + * `{content: }` + **/ +export function createSignalObjectForShape( + shape: Shape, + scope?: Scope, + poolSignal = true +) { + if (poolSignal && openConnections[shape]) return openConnections[shape]; + + // 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 = createSignal<{ + content: ReactiveShapeObject | null; + }>({ content: null }); + + // State + let stopWatcher: any = null; + let suspendDeepWatcher = false; + + const onUpdateFromDb = (diff: Diff, connectionId: Connection["id"]) => { + const rootSignalValue = rootSignal(); + console.log("Update received", connectionId, diff); + // Set new value from applying the diffs to the old value. + + suspendDeepWatcher = true; + // We need to replace the root signal for now, so this is redundant. + batch(() => { + if (!rootSignalValue) return; // This shouldn't happen but we make the compiler happy. + const { content: proxiedShapeObj } = rootSignalValue; + applyDiff(proxiedShapeObj, diff); + + // We put the proxied object into a new object for the root signal to trigger. + rootSignal({ content: proxiedShapeObj }); + }); + suspendDeepWatcher = false; + }; + + // Do the actual db request. + requestShape(shape, scope, onUpdateFromDb).then( + ({ connectionId, shapeObject }) => { + // Create a deepSignal to put into the vanilla alien-signal. + const proxiedShapeObj = deepSignal(shapeObject); + + // Notify DB on changes. + stopWatcher = watch( + proxiedShapeObj, + (newVal, oldVal, onCleanup) => { + // Don't update when applying changes from db diffs from the db. + if (!suspendDeepWatcher) updateShape(connectionId, newVal); + }, + { deep: true } + ); + + // Update the root signal. + rootSignal({ content: proxiedShapeObj }); + } + ); + + if (poolSignal) openConnections[shape] = rootSignal; + + // TODO: Dispose deepSignal root signal disposal. + return rootSignal; +} diff --git a/src/ng-mock/js-land/connector/ngSignals.ts b/src/ng-mock/js-land/connector/ngSignals.ts deleted file mode 100644 index 2813423..0000000 --- a/src/ng-mock/js-land/connector/ngSignals.ts +++ /dev/null @@ -1,75 +0,0 @@ -import updateShape from "src/ng-mock/wasm-land/updateShape"; -import type { Connection, Diff, Scope, Shape } from "../types"; -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"; - -const openConnections: Record> = {}; - -// TODO: The code is horrible. -export function createSignalObjectForShape(shape: Shape, scope?: Scope) { - if (openConnections[shape]) return openConnections[shape]; - - // 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 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); - }; - - // Do the actual db request. - requestShape(shape, scope, onUpdateFromDb).then( - ({ connectionId, shapeObject }) => { - // Create a deepSignal to put into the vanilla alien-signal. - const deepSignalFromDb = deepSignal(shapeObject); - setUpDeepSignal(deepSignalFromDb, connectionId); - } - ); - - openConnections[shape] = rootSignal; - return rootSignal; -} diff --git a/src/ng-mock/js-land/frontendAdapters/react/useShape.ts b/src/ng-mock/js-land/frontendAdapters/react/useShape.ts index 84975af..fdf578e 100644 --- a/src/ng-mock/js-land/frontendAdapters/react/useShape.ts +++ b/src/ng-mock/js-land/frontendAdapters/react/useShape.ts @@ -1,15 +1,18 @@ import { useSignal } from "@gn8/alien-signals-react"; -import { useMemo } from "react"; -import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/ngSignals"; +import { useMemo, useRef } from "react"; +import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape"; import type { Scope, Shape } from "src/ng-mock/js-land/types"; +// TODO: The universe-signal library causes two renders on change instead of one. +// Find a fix + 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); + const [{ content: shapeObject }, setShapeObject] = useSignal(signalOfShape); // We don't need the setter. // The object is recursively proxied and changes are recorded there. diff --git a/src/ng-mock/js-land/frontendAdapters/svelte/useShape.ts b/src/ng-mock/js-land/frontendAdapters/svelte/useShape.ts index 3f93157..ed85284 100644 --- a/src/ng-mock/js-land/frontendAdapters/svelte/useShape.ts +++ b/src/ng-mock/js-land/frontendAdapters/svelte/useShape.ts @@ -1,5 +1,6 @@ +import { derived } from "svelte/store"; import { useSignal } from "@gn8/alien-signals-svelte"; -import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/ngSignals"; +import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape"; import type { Scope, Shape } from "src/ng-mock/js-land/types"; const useShape = (shape: Shape, scope: Scope) => { @@ -7,7 +8,9 @@ const useShape = (shape: Shape, scope: Scope) => { const writeableStoreForShape = useSignal(signalOfShape); - return writeableStoreForShape; + // Get the content "deepSignal" + const content = derived(writeableStoreForShape, (value) => value.content); + return content; }; 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 index e5eacbe..26cd07b 100644 --- a/src/ng-mock/js-land/frontendAdapters/vue/useShape.ts +++ b/src/ng-mock/js-land/frontendAdapters/vue/useShape.ts @@ -1,13 +1,14 @@ import { useSignal } from "@gn8/alien-signals-vue"; -import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/ngSignals"; +import { createSignalObjectForShape } from "src/ng-mock/js-land/connector/createSignalObjectForShape"; import type { Scope, Shape } from "src/ng-mock/js-land/types"; - +import { computed } from "vue"; const useShape = (shape: Shape, scope: Scope) => { const signalOfShape = createSignalObjectForShape(shape, scope); const refOfShape = useSignal(signalOfShape); - return refOfShape; + // TODO: Maybe `refOfShape.value.content` works too? + return computed(() => refOfShape.value.content); }; export default useShape; diff --git a/src/ng-mock/tests/updatesWithWasm.test.ts b/src/ng-mock/tests/updatesWithWasm.test.ts index 686b40b..916f665 100644 --- a/src/ng-mock/tests/updatesWithWasm.test.ts +++ b/src/ng-mock/tests/updatesWithWasm.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import { createSignalObjectForShape } from "../js-land/connector/ngSignals"; +import { createSignalObjectForShape } from "../js-land/connector/createSignalObjectForShape"; const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/ng-mock/wasm-land/requestShape.ts b/src/ng-mock/wasm-land/requestShape.ts index 10e31af..c739145 100644 --- a/src/ng-mock/wasm-land/requestShape.ts +++ b/src/ng-mock/wasm-land/requestShape.ts @@ -24,6 +24,8 @@ const mockShapeObject2 = { }, }; +let connectionIdCounter = 1; + export default async function requestShape( shape: Shape, scope: Scope | undefined, @@ -33,7 +35,7 @@ export default async function requestShape( shapeObject: object; }> { const connection: WasmConnection = { - id: Math.random().toFixed(4), + id: String(connectionIdCounter++), shape, // Create a deep copy to prevent accidental by-reference changes. state: JSON.parse(