From e979f0233afcfcab60e54392104e62f0160c1129 Mon Sep 17 00:00:00 2001 From: Laurin Weger Date: Sun, 19 Oct 2025 18:08:46 +0200 Subject: [PATCH] deep signals improvements --- sdk/js/alien-deepsignals/README.md | 8 +- sdk/js/alien-deepsignals/src/deepSignal.ts | 8 +- .../src/test/patchOptimized.test.ts | 62 ++-- .../alien-deepsignals/src/test/tier3.test.ts | 234 +++++++-------- .../alien-deepsignals/src/test/watch.test.ts | 154 +++++----- sdk/js/alien-deepsignals/src/watch.ts | 268 +++++++++--------- 6 files changed, 368 insertions(+), 366 deletions(-) diff --git a/sdk/js/alien-deepsignals/README.md b/sdk/js/alien-deepsignals/README.md index 9c13e9a..815950e 100644 --- a/sdk/js/alien-deepsignals/README.md +++ b/sdk/js/alien-deepsignals/README.md @@ -53,7 +53,7 @@ console.log(state.$count!()); // read via signal function ```ts type DeepSignalOptions = { - idGenerator?: () => string | number; // Custom ID generator function + idGenerator?: (pathToObject: (string | number)[]) => string | number; // Custom ID generator function addIdToObjects?: boolean; // Automatically add @id to plain objects }; ``` @@ -84,7 +84,7 @@ When `addIdToObjects: true`, plain objects automatically receive a readonly, enu const state = deepSignal( { data: {} }, { - idGenerator: () => `urn:uuid:${crypto.randomUUID()}`, + idGenerator: (_path) => `urn:uuid:${crypto.randomUUID()}`, addIdToObjects: true } ); @@ -191,7 +191,7 @@ import { addWithId, setSetEntrySyntheticId } from "@ng-org/alien-deepsignals"; const state = deepSignal( { items: new Set() }, { - idGenerator: () => `urn:uuid:${crypto.randomUUID()}`, + idGenerator: (_path) => `urn:uuid:${crypto.randomUUID()}`, addIdToObjects: true } ); @@ -250,7 +250,7 @@ state.s.add({ data: "test" }); const state = deepSignal( { users: new Set() }, { - idGenerator: () => `urn:user:${crypto.randomUUID()}`, + idGenerator: (path) => `urn:user:${path.join("-")}:${crypto.randomUUID()}`, addIdToObjects: true } ); diff --git a/sdk/js/alien-deepsignals/src/deepSignal.ts b/sdk/js/alien-deepsignals/src/deepSignal.ts index 792cbb5..b37f99d 100644 --- a/sdk/js/alien-deepsignals/src/deepSignal.ts +++ b/sdk/js/alien-deepsignals/src/deepSignal.ts @@ -59,7 +59,7 @@ export type DeepPatchSubscriber = (patches: DeepPatch[]) => void; /** Options for configuring deepSignal behavior. */ export interface DeepSignalOptions { /** Custom function to generate synthetic IDs for objects without @id. */ - idGenerator?: () => string | number; + idGenerator?: (pathToObject: (string | number)[]) => string | number; /** If true, add @id property to all objects in the tree. */ addIdToObjects?: boolean; } @@ -156,7 +156,7 @@ function queueDeepPatches( ) { let syntheticId: string | number; if (options.idGenerator) { - syntheticId = options.idGenerator(); + syntheticId = options.idGenerator(basePath); } else { syntheticId = assignBlankNodeId(val); } @@ -516,6 +516,7 @@ function getFromSet( }; // Pre-pass to ensure any existing non-proxied object entries are proxied (enables deep patches after iteration) if (meta) raw.forEach(ensureEntryProxy); + if (key === "add" || key === "delete" || key === "clear") { const fn: Function = (raw as any)[key]; return function (this: any, ...args: any[]) { @@ -545,7 +546,8 @@ function getFromSet( ) { let syntheticId: string | number; if (metaNow.options.idGenerator) { - syntheticId = metaNow.options.idGenerator(); + syntheticId = + metaNow.options.idGenerator(containerPath); } else { syntheticId = assignBlankNodeId(entry); } diff --git a/sdk/js/alien-deepsignals/src/test/patchOptimized.test.ts b/sdk/js/alien-deepsignals/src/test/patchOptimized.test.ts index 6922473..5e129c7 100644 --- a/sdk/js/alien-deepsignals/src/test/patchOptimized.test.ts +++ b/sdk/js/alien-deepsignals/src/test/patchOptimized.test.ts @@ -8,40 +8,40 @@ import { watch } from "../watch"; // times traverse() executes under each strategy. describe("watch patch-only simplified performance placeholder", () => { - let store: any; - const build = (breadth = 3, depth = 3) => { - const make = (d: number): any => { - if (d === 0) return { v: 0 }; - const obj: any = {}; - for (let i = 0; i < breadth; i++) obj["k" + i] = make(d - 1); - return obj; + let store: any; + const build = (breadth = 3, depth = 3) => { + const make = (d: number): any => { + if (d === 0) return { v: 0 }; + const obj: any = {}; + for (let i = 0; i < breadth; i++) obj["k" + i] = make(d - 1); + return obj; + }; + return make(depth); }; - return make(depth); - }; - beforeEach(() => { - store = deepSignal(build()); - }); + beforeEach(() => { + store = deepSignal(build()); + }); - function mutateAll(breadth = 3, depth = 3) { - const visit = (node: any, d: number) => { - if (d === 0) { - node.v++; - return; - } - for (let i = 0; i < breadth; i++) visit(node["k" + i], d - 1); - }; - visit(store, depth); - } + function mutateAll(breadth = 3, depth = 3) { + const visit = (node: any, d: number) => { + if (d === 0) { + node.v++; + return; + } + for (let i = 0; i < breadth; i++) visit(node["k" + i], d - 1); + }; + visit(store, depth); + } - it("receives a single batch of patches after deep mutations", async () => { - let batches = 0; - const { stopListening: stop } = watch(store, ({ patches }) => { - if (patches.length) batches++; + it("receives a single batch of patches after deep mutations", async () => { + let batches = 0; + const { stopListening: stop } = watch(store, ({ patches }) => { + if (patches.length) batches++; + }); + mutateAll(); + await Promise.resolve(); + expect(batches).toBe(1); + stop(); }); - mutateAll(); - await Promise.resolve(); - expect(batches).toBe(1); - stop(); - }); }); diff --git a/sdk/js/alien-deepsignals/src/test/tier3.test.ts b/sdk/js/alien-deepsignals/src/test/tier3.test.ts index ba54feb..1ce5273 100644 --- a/sdk/js/alien-deepsignals/src/test/tier3.test.ts +++ b/sdk/js/alien-deepsignals/src/test/tier3.test.ts @@ -1,148 +1,148 @@ import { describe, it, expect } from "vitest"; import { deepSignal, getDeepSignalRootId, peek } from "../deepSignal"; import { - watch, - __traverseCount, - __resetTraverseCount, - traverse, + watch, + __traverseCount, + __resetTraverseCount, + traverse, } from "../watch"; import { effect } from "../core"; describe("watch advanced", () => { - it("basic patch watcher fires on deep mutations", async () => { - const st = deepSignal({ a: { b: { c: 1 } } }); - let batches: number = 0; - watch(st, ({ patches }) => { - if (patches.length) batches++; + it("basic patch watcher fires on deep mutations", async () => { + const st = deepSignal({ a: { b: { c: 1 } } }); + let batches: number = 0; + watch(st, ({ patches }) => { + if (patches.length) batches++; + }); + st.a.b.c = 2; + st.a.b = { c: 3 } as any; + await Promise.resolve(); + expect(batches).toBeGreaterThan(0); }); - st.a.b.c = 2; - st.a.b = { c: 3 } as any; - await Promise.resolve(); - expect(batches).toBeGreaterThan(0); - }); - // multi-source value mode removed; patch-only now - skip equivalent + // multi-source value mode removed; patch-only now - skip equivalent - // getter source value mode removed in patch-only watcher + // getter source value mode removed in patch-only watcher - it("watch once option still stops after first batch", async () => { - const st = deepSignal({ a: 1 }); - let count = 0; - watch( - st, - () => { - count++; - }, - { once: true, immediate: true } - ); - st.a = 2; - st.a = 3; - await Promise.resolve(); - expect(count).toBe(1); - }); + it("watch once option still stops after first batch", async () => { + const st = deepSignal({ a: 1 }); + let count = 0; + watch( + st, + () => { + count++; + }, + { once: true, immediate: true } + ); + st.a = 2; + st.a = 3; + await Promise.resolve(); + expect(count).toBe(1); + }); - // observe value mode removed; observe is alias of watch + // observe value mode removed; observe is alias of watch }); describe("patches & root ids", () => { - it("root ids are unique", () => { - const a = deepSignal({}); - const b = deepSignal({}); - expect(getDeepSignalRootId(a)).not.toBe(getDeepSignalRootId(b)); - }); + it("root ids are unique", () => { + const a = deepSignal({}); + const b = deepSignal({}); + expect(getDeepSignalRootId(a)).not.toBe(getDeepSignalRootId(b)); + }); - // legacy watchPatches API removed; patch mode only valid for deepSignal roots - it("watch throws on non-deepSignal input", () => { - expect(() => watch({} as any, () => {})).toThrow(); - }); + // legacy watchPatches API removed; patch mode only valid for deepSignal roots + it("watch throws on non-deepSignal input", () => { + expect(() => watch({} as any, () => {})).toThrow(); + }); - it("Map unsupported does not emit patches", async () => { - const m = new Map(); - const st = deepSignal({ m }); - const patches: any[] = []; - const { stopListening: stop } = watch(st, ({ patches: batch }) => - patches.push(batch) - ); - m.set("a", 1); - await Promise.resolve(); - await Promise.resolve(); - expect(patches.length).toBe(0); - stop(); - }); + it("Map unsupported does not emit patches", async () => { + const m = new Map(); + const st = deepSignal({ m }); + const patches: any[] = []; + const { stopListening: stop } = watch(st, ({ patches: batch }) => + patches.push(batch) + ); + m.set("a", 1); + await Promise.resolve(); + await Promise.resolve(); + expect(patches.length).toBe(0); + stop(); + }); }); describe("tier3: Set iteration variants", () => { - it("entries() iteration proxies nested mutation", async () => { - const st = deepSignal({ s: new Set() }); - st.s.add({ id: "eEnt", inner: { v: 1 } }); - const paths: string[] = []; - const { stopListening: stop } = watch(st, ({ patches }) => - paths.push(...patches.map((pp: any) => pp.path.join("."))) - ); - for (const [val] of st.s.entries()) { - (val as any).inner.v; - } // ensure proxy - for (const [val] of st.s.entries()) { - (val as any).inner.v = 2; - } - await Promise.resolve(); - await Promise.resolve(); - expect(paths.some((p) => p.endsWith("eEnt.inner.v"))).toBe(true); - stop(); - }); + it("entries() iteration proxies nested mutation", async () => { + const st = deepSignal({ s: new Set() }); + st.s.add({ id: "eEnt", inner: { v: 1 } }); + const paths: string[] = []; + const { stopListening: stop } = watch(st, ({ patches }) => + paths.push(...patches.map((pp: any) => pp.path.join("."))) + ); + for (const [val] of st.s.entries()) { + (val as any).inner.v; + } // ensure proxy + for (const [val] of st.s.entries()) { + (val as any).inner.v = 2; + } + await Promise.resolve(); + await Promise.resolve(); + expect(paths.some((p) => p.endsWith("eEnt.inner.v"))).toBe(true); + stop(); + }); - it("forEach iteration proxies nested mutation", async () => { - const st = deepSignal({ s: new Set() }); - st.s.add({ id: "fe1", data: { n: 1 } }); - const { stopListening: stop } = watch(st, () => {}); - st.s.forEach((e) => (e as any).data.n); // access - st.s.forEach((e) => { - (e as any).data.n = 2; + it("forEach iteration proxies nested mutation", async () => { + const st = deepSignal({ s: new Set() }); + st.s.add({ id: "fe1", data: { n: 1 } }); + const { stopListening: stop } = watch(st, () => {}); + st.s.forEach((e) => (e as any).data.n); // access + st.s.forEach((e) => { + (e as any).data.n = 2; + }); + await Promise.resolve(); + await Promise.resolve(); + stop(); }); - await Promise.resolve(); - await Promise.resolve(); - stop(); - }); - it("keys() iteration returns proxies", async () => { - const st = deepSignal({ s: new Set() }); - st.s.add({ id: "k1", foo: { x: 1 } }); - const { stopListening: stop } = watch(st, () => {}); - for (const e of st.s.keys()) { - (e as any).foo.x = 2; - } - await Promise.resolve(); - await Promise.resolve(); - stop(); - }); + it("keys() iteration returns proxies", async () => { + const st = deepSignal({ s: new Set() }); + st.s.add({ id: "k1", foo: { x: 1 } }); + const { stopListening: stop } = watch(st, () => {}); + for (const e of st.s.keys()) { + (e as any).foo.x = 2; + } + await Promise.resolve(); + await Promise.resolve(); + stop(); + }); }); describe("tier3: peek behavior", () => { - it("peek does not create reactive dependency on property", async () => { - const st = deepSignal({ a: 1 }); - let runs = 0; - effect(() => { - runs++; - peek(st, "a"); + it("peek does not create reactive dependency on property", async () => { + const st = deepSignal({ a: 1 }); + let runs = 0; + effect(() => { + runs++; + peek(st, "a"); + }); + expect(runs).toBe(1); + st.a = 2; + // Flush microtasks + await Promise.resolve(); + await Promise.resolve(); + expect(runs).toBe(1); // no rerun }); - expect(runs).toBe(1); - st.a = 2; - // Flush microtasks - await Promise.resolve(); - await Promise.resolve(); - expect(runs).toBe(1); // no rerun - }); }); describe("tier3: traverse helper direct calls (symbols & sets)", () => { - it("traverse counts and respects depth param", () => { - __resetTraverseCount(); - const obj: any = { a: { b: { c: 1 } } }; - traverse(obj, 1); - const shallowCount = __traverseCount; - __resetTraverseCount(); - traverse(obj, 3); - const deepCount = __traverseCount; - expect(deepCount).toBeGreaterThan(shallowCount); - }); + it("traverse counts and respects depth param", () => { + __resetTraverseCount(); + const obj: any = { a: { b: { c: 1 } } }; + traverse(obj, 1); + const shallowCount = __traverseCount; + __resetTraverseCount(); + traverse(obj, 3); + const deepCount = __traverseCount; + expect(deepCount).toBeGreaterThan(shallowCount); + }); }); diff --git a/sdk/js/alien-deepsignals/src/test/watch.test.ts b/sdk/js/alien-deepsignals/src/test/watch.test.ts index 4ba399f..06291de 100644 --- a/sdk/js/alien-deepsignals/src/test/watch.test.ts +++ b/sdk/js/alien-deepsignals/src/test/watch.test.ts @@ -4,88 +4,88 @@ import { watch } from "../watch"; import { watchEffect } from "../watchEffect"; describe("watch", () => { - it("watch immediate", () => { - const store = deepSignal({ - userinfo: { - name: "tom", - }, + it("watch immediate", () => { + const store = deepSignal({ + userinfo: { + name: "tom", + }, + }); + let val!: string; + watch( + store, + ({ newValue }) => { + val = newValue.userinfo.name; + }, + { immediate: true } + ); + expect(val).toEqual("tom"); }); - let val!: string; - watch( - store, - ({ newValue }) => { - val = newValue.userinfo.name; - }, - { immediate: true } - ); - expect(val).toEqual("tom"); - }); - it("watch deep", () => { - const store = deepSignal({ - userinfo: { - name: "tom", - }, + it("watch deep", () => { + const store = deepSignal({ + userinfo: { + name: "tom", + }, + }); + let val!: string; + watch( + store, + ({ newValue }) => { + val = newValue.userinfo.name; + }, + { immediate: true } + ); + let value2!: string; + watch( + store, + ({ newValue }) => { + value2 = newValue.userinfo.name; + }, + { immediate: true } + ); + expect(val).toEqual("tom"); + store.userinfo.name = "jon"; + // patch delivery async (microtask) + return Promise.resolve().then(() => { + expect(val).toEqual("jon"); + // With refactored watch using native effect, shallow watcher now also updates root reference + expect(value2).toEqual("jon"); + }); }); - let val!: string; - watch( - store, - ({ newValue }) => { - val = newValue.userinfo.name; - }, - { immediate: true } - ); - let value2!: string; - watch( - store, - ({ newValue }) => { - value2 = newValue.userinfo.name; - }, - { immediate: true } - ); - expect(val).toEqual("tom"); - store.userinfo.name = "jon"; - // patch delivery async (microtask) - return Promise.resolve().then(() => { - expect(val).toEqual("jon"); - // With refactored watch using native effect, shallow watcher now also updates root reference - expect(value2).toEqual("jon"); - }); - }); - it("watch once", () => { - const store = deepSignal({ - userinfo: { - name: "tom", - }, + it("watch once", () => { + const store = deepSignal({ + userinfo: { + name: "tom", + }, + }); + let val!: string; + watch( + store, + ({ newValue }) => { + val = newValue.userinfo.name; + }, + { immediate: true, once: true } + ); + + expect(val).toEqual("tom"); + store.userinfo.name = "jon"; + // once watcher shouldn't update after first run + expect(val).toEqual("tom"); }); - let val!: string; - watch( - store, - ({ newValue }) => { - val = newValue.userinfo.name; - }, - { immediate: true, once: true } - ); - expect(val).toEqual("tom"); - store.userinfo.name = "jon"; - // once watcher shouldn't update after first run - expect(val).toEqual("tom"); - }); + it("watch effect", () => { + const store = deepSignal({ + userinfo: { + name: "tom", + }, + }); + let x = undefined; + watchEffect(() => { + x = store.userinfo.name; + }); - it("watch effect", () => { - const store = deepSignal({ - userinfo: { - name: "tom", - }, + expect(x).toEqual("tom"); + store.userinfo.name = "jon"; + expect(x).toEqual("jon"); }); - let x = undefined; - watchEffect(() => { - x = store.userinfo.name; - }); - - expect(x).toEqual("tom"); - store.userinfo.name = "jon"; - expect(x).toEqual("jon"); - }); }); diff --git a/sdk/js/alien-deepsignals/src/watch.ts b/sdk/js/alien-deepsignals/src/watch.ts index f3784c1..af737a0 100644 --- a/sdk/js/alien-deepsignals/src/watch.ts +++ b/sdk/js/alien-deepsignals/src/watch.ts @@ -1,10 +1,10 @@ import { isObject, isPlainObject, isSet, isMap, isArray } from "./utils"; import { isSignal } from "./core"; import { - isDeepSignal, - subscribeDeepMutations, - getDeepSignalRootId, - DeepPatch, + isDeepSignal, + subscribeDeepMutations, + getDeepSignalRootId, + DeepPatch, } from "./deepSignal"; import { ReactiveFlags } from "./contents"; @@ -15,125 +15,125 @@ export type WatchEffect = (registerCleanup: RegisterCleanup) => void; /** Options for {@link watch}. */ export interface WatchOptions { - /** Trigger the callback immediately with the current value (default: false). */ - immediate?: boolean; - /** Auto-stop the watcher after the first callback run that delivers patches (or immediate call if no patches). */ - once?: boolean; - /** Allow legacy/unknown options (ignored) to avoid hard breaks while migrating. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [legacy: string]: any; + /** Trigger the callback immediately with the current value (default: false). */ + immediate?: boolean; + /** Auto-stop the watcher after the first callback run that delivers patches (or immediate call if no patches). */ + once?: boolean; + /** Allow legacy/unknown options (ignored) to avoid hard breaks while migrating. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [legacy: string]: any; } export interface WatchPatchEvent { - /** Patch batch that triggered this callback (may be empty for immediate). */ - patches: DeepPatch[]; - /** Previous snapshot (deep-cloned) of the root value before these patches. Undefined on first call. */ - oldValue: Root | undefined; - /** Current root value (live proxy). */ - newValue: Root; + /** Patch batch that triggered this callback (may be empty for immediate). */ + patches: DeepPatch[]; + /** Previous snapshot (deep-cloned) of the root value before these patches. Undefined on first call. */ + oldValue: Root | undefined; + /** Current root value (live proxy). */ + newValue: Root; } export type WatchPatchCallback = ( - event: WatchPatchEvent + event: WatchPatchEvent ) => any; // Internal helper kept for external compatibility. export const remove = (arr: T[], el: T): void => { - const i = arr.indexOf(el); - if (i > -1) arr.splice(i, 1); + const i = arr.indexOf(el); + if (i > -1) arr.splice(i, 1); }; /** Observe patch batches on a deep signal root. */ export function watch( - source: Root, - callback: WatchPatchCallback, - options: WatchOptions = {} + source: Root, + callback: WatchPatchCallback, + options: WatchOptions = {} ) { - if (!isDeepSignal(source)) { - throw new Error( - "watch() now only supports deepSignal roots (patch mode only)" - ); - } - const { immediate, once } = options; - - const rootId = getDeepSignalRootId(source as any)!; - - let active = true; - let cleanup: (() => void) | undefined; - const registerCleanup: RegisterCleanup = (fn) => { - cleanup = fn; - }; - const runCleanup = () => { - if (cleanup) { - try { - cleanup(); - } catch { - /* ignore */ - } finally { - cleanup = undefined; - } + if (!isDeepSignal(source)) { + throw new Error( + "watch() now only supports deepSignal roots (patch mode only)" + ); } - }; - - // Deep clone snapshot helper (JSON clone sufficient for typical reactive plain data) - const clone = (v: any) => { - try { - return JSON.parse(JSON.stringify(v)); - } catch { - return undefined as any; - } - }; - let lastSnapshot: Root | undefined = clone(source); - - const stopListening = () => { - if (!active) return; - active = false; - runCleanup(); - unsubscribe && unsubscribe(); - }; - - const deliver = (patches: DeepPatch[]) => { - if (!active) return; - runCleanup(); - const prev = lastSnapshot; - const next = source as any as Root; // live proxy - try { - callback({ - patches, - oldValue: prev, - newValue: next, - }); - } finally { - if (active) lastSnapshot = clone(next); - if (once) stopListening(); + const { immediate, once } = options; + + const rootId = getDeepSignalRootId(source as any)!; + + let active = true; + let cleanup: (() => void) | undefined; + const registerCleanup: RegisterCleanup = (fn) => { + cleanup = fn; + }; + const runCleanup = () => { + if (cleanup) { + try { + cleanup(); + } catch { + /* ignore */ + } finally { + cleanup = undefined; + } + } + }; + + // Deep clone snapshot helper (JSON clone sufficient for typical reactive plain data) + const clone = (v: any) => { + try { + return JSON.parse(JSON.stringify(v)); + } catch { + return undefined as any; + } + }; + let lastSnapshot: Root | undefined = clone(source); + + const stopListening = () => { + if (!active) return; + active = false; + runCleanup(); + unsubscribe && unsubscribe(); + }; + + const deliver = (patches: DeepPatch[]) => { + if (!active) return; + runCleanup(); + const prev = lastSnapshot; + const next = source as any as Root; // live proxy + try { + callback({ + patches, + oldValue: prev, + newValue: next, + }); + } finally { + if (active) lastSnapshot = clone(next); + if (once) stopListening(); + } + }; + + const unsubscribe = subscribeDeepMutations(rootId, (patches) => { + if (!patches.length) return; // ignore empty batches + deliver(patches); + }); + + if (immediate) { + // Immediate call with empty patch list (snapshot only) + deliver([]); } - }; - - const unsubscribe = subscribeDeepMutations(rootId, (patches) => { - if (!patches.length) return; // ignore empty batches - deliver(patches); - }); - - if (immediate) { - // Immediate call with empty patch list (snapshot only) - deliver([]); - } - - return { - /** Stop listening to future patch batches; idempotent. */ - stopListening, - /** Register a cleanup callback run before the next invocation / stop. */ - registerCleanup, - }; + + return { + /** Stop listening to future patch batches; idempotent. */ + stopListening, + /** Register a cleanup callback run before the next invocation / stop. */ + registerCleanup, + }; } // observe alias export function observe( - source: any, - cb: WatchPatchCallback, - options?: WatchOptions + source: any, + cb: WatchPatchCallback, + options?: WatchOptions ) { - return watch(source, cb, options); + return watch(source, cb, options); } // Instrumentation counter for performance tests (number of traverse invocations) @@ -141,7 +141,7 @@ export function observe( export let __traverseCount = 0; // retained for external tooling/tests although watch no longer uses traversal /** Reset the traversal instrumentation counter back to 0. */ export function __resetTraverseCount() { - __traverseCount = 0; + __traverseCount = 0; } /** @@ -149,40 +149,40 @@ export function __resetTraverseCount() { * Depth-limited; protects against cycles via `seen` set; respects ReactiveFlags.SKIP opt-out. */ export function traverse( - value: unknown, - depth: number = Infinity, - seen?: Set + value: unknown, + depth: number = Infinity, + seen?: Set ): unknown { - __traverseCount++; - if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) { - return value; - } - - seen = seen || new Set(); - if (seen.has(value)) { - return value; - } - seen.add(value); - depth--; - if (isSignal(value)) { - traverse((value as any)(), depth, seen); - } else if (isArray(value)) { - for (let i = 0; i < value.length; i++) { - traverse(value[i], depth, seen); + __traverseCount++; + if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) { + return value; } - } else if (isSet(value) || isMap(value)) { - value.forEach((v: any) => { - traverse(v, depth, seen); - }); - } else if (isPlainObject(value)) { - for (const key in value) { - traverse(value[key], depth, seen); + + seen = seen || new Set(); + if (seen.has(value)) { + return value; } - for (const key of Object.getOwnPropertySymbols(value)) { - if (Object.prototype.propertyIsEnumerable.call(value, key)) { - traverse(value[key as any], depth, seen); - } + seen.add(value); + depth--; + if (isSignal(value)) { + traverse((value as any)(), depth, seen); + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], depth, seen); + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v: any) => { + traverse(v, depth, seen); + }); + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], depth, seen); + } + for (const key of Object.getOwnPropertySymbols(value)) { + if (Object.prototype.propertyIsEnumerable.call(value, key)) { + traverse(value[key as any], depth, seen); + } + } } - } - return value; + return value; }