deep signals improvements

refactor
Laurin Weger 1 day ago
parent b4bcbecaaf
commit e979f0233a
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 8
      sdk/js/alien-deepsignals/README.md
  2. 8
      sdk/js/alien-deepsignals/src/deepSignal.ts
  3. 62
      sdk/js/alien-deepsignals/src/test/patchOptimized.test.ts
  4. 234
      sdk/js/alien-deepsignals/src/test/tier3.test.ts
  5. 154
      sdk/js/alien-deepsignals/src/test/watch.test.ts
  6. 268
      sdk/js/alien-deepsignals/src/watch.ts

@ -53,7 +53,7 @@ console.log(state.$count!()); // read via signal function
```ts ```ts
type DeepSignalOptions = { 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 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( const state = deepSignal(
{ data: {} }, { data: {} },
{ {
idGenerator: () => `urn:uuid:${crypto.randomUUID()}`, idGenerator: (_path) => `urn:uuid:${crypto.randomUUID()}`,
addIdToObjects: true addIdToObjects: true
} }
); );
@ -191,7 +191,7 @@ import { addWithId, setSetEntrySyntheticId } from "@ng-org/alien-deepsignals";
const state = deepSignal( const state = deepSignal(
{ items: new Set() }, { items: new Set() },
{ {
idGenerator: () => `urn:uuid:${crypto.randomUUID()}`, idGenerator: (_path) => `urn:uuid:${crypto.randomUUID()}`,
addIdToObjects: true addIdToObjects: true
} }
); );
@ -250,7 +250,7 @@ state.s.add({ data: "test" });
const state = deepSignal( const state = deepSignal(
{ users: new Set() }, { users: new Set() },
{ {
idGenerator: () => `urn:user:${crypto.randomUUID()}`, idGenerator: (path) => `urn:user:${path.join("-")}:${crypto.randomUUID()}`,
addIdToObjects: true addIdToObjects: true
} }
); );

@ -59,7 +59,7 @@ export type DeepPatchSubscriber = (patches: DeepPatch[]) => void;
/** Options for configuring deepSignal behavior. */ /** Options for configuring deepSignal behavior. */
export interface DeepSignalOptions { export interface DeepSignalOptions {
/** Custom function to generate synthetic IDs for objects without @id. */ /** 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. */ /** If true, add @id property to all objects in the tree. */
addIdToObjects?: boolean; addIdToObjects?: boolean;
} }
@ -156,7 +156,7 @@ function queueDeepPatches(
) { ) {
let syntheticId: string | number; let syntheticId: string | number;
if (options.idGenerator) { if (options.idGenerator) {
syntheticId = options.idGenerator(); syntheticId = options.idGenerator(basePath);
} else { } else {
syntheticId = assignBlankNodeId(val); 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) // Pre-pass to ensure any existing non-proxied object entries are proxied (enables deep patches after iteration)
if (meta) raw.forEach(ensureEntryProxy); if (meta) raw.forEach(ensureEntryProxy);
if (key === "add" || key === "delete" || key === "clear") { if (key === "add" || key === "delete" || key === "clear") {
const fn: Function = (raw as any)[key]; const fn: Function = (raw as any)[key];
return function (this: any, ...args: any[]) { return function (this: any, ...args: any[]) {
@ -545,7 +546,8 @@ function getFromSet(
) { ) {
let syntheticId: string | number; let syntheticId: string | number;
if (metaNow.options.idGenerator) { if (metaNow.options.idGenerator) {
syntheticId = metaNow.options.idGenerator(); syntheticId =
metaNow.options.idGenerator(containerPath);
} else { } else {
syntheticId = assignBlankNodeId(entry); syntheticId = assignBlankNodeId(entry);
} }

@ -8,40 +8,40 @@ import { watch } from "../watch";
// times traverse() executes under each strategy. // times traverse() executes under each strategy.
describe("watch patch-only simplified performance placeholder", () => { describe("watch patch-only simplified performance placeholder", () => {
let store: any; let store: any;
const build = (breadth = 3, depth = 3) => { const build = (breadth = 3, depth = 3) => {
const make = (d: number): any => { const make = (d: number): any => {
if (d === 0) return { v: 0 }; if (d === 0) return { v: 0 };
const obj: any = {}; const obj: any = {};
for (let i = 0; i < breadth; i++) obj["k" + i] = make(d - 1); for (let i = 0; i < breadth; i++) obj["k" + i] = make(d - 1);
return obj; return obj;
};
return make(depth);
}; };
return make(depth);
};
beforeEach(() => { beforeEach(() => {
store = deepSignal(build()); store = deepSignal(build());
}); });
function mutateAll(breadth = 3, depth = 3) { function mutateAll(breadth = 3, depth = 3) {
const visit = (node: any, d: number) => { const visit = (node: any, d: number) => {
if (d === 0) { if (d === 0) {
node.v++; node.v++;
return; return;
} }
for (let i = 0; i < breadth; i++) visit(node["k" + i], d - 1); for (let i = 0; i < breadth; i++) visit(node["k" + i], d - 1);
}; };
visit(store, depth); visit(store, depth);
} }
it("receives a single batch of patches after deep mutations", async () => { it("receives a single batch of patches after deep mutations", async () => {
let batches = 0; let batches = 0;
const { stopListening: stop } = watch(store, ({ patches }) => { const { stopListening: stop } = watch(store, ({ patches }) => {
if (patches.length) batches++; if (patches.length) batches++;
});
mutateAll();
await Promise.resolve();
expect(batches).toBe(1);
stop();
}); });
mutateAll();
await Promise.resolve();
expect(batches).toBe(1);
stop();
});
}); });

@ -1,148 +1,148 @@
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { deepSignal, getDeepSignalRootId, peek } from "../deepSignal"; import { deepSignal, getDeepSignalRootId, peek } from "../deepSignal";
import { import {
watch, watch,
__traverseCount, __traverseCount,
__resetTraverseCount, __resetTraverseCount,
traverse, traverse,
} from "../watch"; } from "../watch";
import { effect } from "../core"; import { effect } from "../core";
describe("watch advanced", () => { describe("watch advanced", () => {
it("basic patch watcher fires on deep mutations", async () => { it("basic patch watcher fires on deep mutations", async () => {
const st = deepSignal({ a: { b: { c: 1 } } }); const st = deepSignal({ a: { b: { c: 1 } } });
let batches: number = 0; let batches: number = 0;
watch(st, ({ patches }) => { watch(st, ({ patches }) => {
if (patches.length) batches++; 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 () => { it("watch once option still stops after first batch", async () => {
const st = deepSignal({ a: 1 }); const st = deepSignal({ a: 1 });
let count = 0; let count = 0;
watch( watch(
st, st,
() => { () => {
count++; count++;
}, },
{ once: true, immediate: true } { once: true, immediate: true }
); );
st.a = 2; st.a = 2;
st.a = 3; st.a = 3;
await Promise.resolve(); await Promise.resolve();
expect(count).toBe(1); 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", () => { describe("patches & root ids", () => {
it("root ids are unique", () => { it("root ids are unique", () => {
const a = deepSignal({}); const a = deepSignal({});
const b = deepSignal({}); const b = deepSignal({});
expect(getDeepSignalRootId(a)).not.toBe(getDeepSignalRootId(b)); expect(getDeepSignalRootId(a)).not.toBe(getDeepSignalRootId(b));
}); });
// legacy watchPatches API removed; patch mode only valid for deepSignal roots // legacy watchPatches API removed; patch mode only valid for deepSignal roots
it("watch throws on non-deepSignal input", () => { it("watch throws on non-deepSignal input", () => {
expect(() => watch({} as any, () => {})).toThrow(); expect(() => watch({} as any, () => {})).toThrow();
}); });
it("Map unsupported does not emit patches", async () => { it("Map unsupported does not emit patches", async () => {
const m = new Map<string, number>(); const m = new Map<string, number>();
const st = deepSignal({ m }); const st = deepSignal({ m });
const patches: any[] = []; const patches: any[] = [];
const { stopListening: stop } = watch(st, ({ patches: batch }) => const { stopListening: stop } = watch(st, ({ patches: batch }) =>
patches.push(batch) patches.push(batch)
); );
m.set("a", 1); m.set("a", 1);
await Promise.resolve(); await Promise.resolve();
await Promise.resolve(); await Promise.resolve();
expect(patches.length).toBe(0); expect(patches.length).toBe(0);
stop(); stop();
}); });
}); });
describe("tier3: Set iteration variants", () => { describe("tier3: Set iteration variants", () => {
it("entries() iteration proxies nested mutation", async () => { it("entries() iteration proxies nested mutation", async () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "eEnt", inner: { v: 1 } }); st.s.add({ id: "eEnt", inner: { v: 1 } });
const paths: string[] = []; const paths: string[] = [];
const { stopListening: stop } = watch(st, ({ patches }) => const { stopListening: stop } = watch(st, ({ patches }) =>
paths.push(...patches.map((pp: any) => pp.path.join("."))) paths.push(...patches.map((pp: any) => pp.path.join(".")))
); );
for (const [val] of st.s.entries()) { for (const [val] of st.s.entries()) {
(val as any).inner.v; (val as any).inner.v;
} // ensure proxy } // ensure proxy
for (const [val] of st.s.entries()) { for (const [val] of st.s.entries()) {
(val as any).inner.v = 2; (val as any).inner.v = 2;
} }
await Promise.resolve(); await Promise.resolve();
await Promise.resolve(); await Promise.resolve();
expect(paths.some((p) => p.endsWith("eEnt.inner.v"))).toBe(true); expect(paths.some((p) => p.endsWith("eEnt.inner.v"))).toBe(true);
stop(); stop();
}); });
it("forEach iteration proxies nested mutation", async () => { it("forEach iteration proxies nested mutation", async () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "fe1", data: { n: 1 } }); st.s.add({ id: "fe1", data: { n: 1 } });
const { stopListening: stop } = watch(st, () => {}); const { stopListening: stop } = watch(st, () => {});
st.s.forEach((e) => (e as any).data.n); // access st.s.forEach((e) => (e as any).data.n); // access
st.s.forEach((e) => { st.s.forEach((e) => {
(e as any).data.n = 2; (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 () => { it("keys() iteration returns proxies", async () => {
const st = deepSignal({ s: new Set<any>() }); const st = deepSignal({ s: new Set<any>() });
st.s.add({ id: "k1", foo: { x: 1 } }); st.s.add({ id: "k1", foo: { x: 1 } });
const { stopListening: stop } = watch(st, () => {}); const { stopListening: stop } = watch(st, () => {});
for (const e of st.s.keys()) { for (const e of st.s.keys()) {
(e as any).foo.x = 2; (e as any).foo.x = 2;
} }
await Promise.resolve(); await Promise.resolve();
await Promise.resolve(); await Promise.resolve();
stop(); stop();
}); });
}); });
describe("tier3: peek behavior", () => { describe("tier3: peek behavior", () => {
it("peek does not create reactive dependency on property", async () => { it("peek does not create reactive dependency on property", async () => {
const st = deepSignal({ a: 1 }); const st = deepSignal({ a: 1 });
let runs = 0; let runs = 0;
effect(() => { effect(() => {
runs++; runs++;
peek(st, "a"); 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)", () => { describe("tier3: traverse helper direct calls (symbols & sets)", () => {
it("traverse counts and respects depth param", () => { it("traverse counts and respects depth param", () => {
__resetTraverseCount(); __resetTraverseCount();
const obj: any = { a: { b: { c: 1 } } }; const obj: any = { a: { b: { c: 1 } } };
traverse(obj, 1); traverse(obj, 1);
const shallowCount = __traverseCount; const shallowCount = __traverseCount;
__resetTraverseCount(); __resetTraverseCount();
traverse(obj, 3); traverse(obj, 3);
const deepCount = __traverseCount; const deepCount = __traverseCount;
expect(deepCount).toBeGreaterThan(shallowCount); expect(deepCount).toBeGreaterThan(shallowCount);
}); });
}); });

@ -4,88 +4,88 @@ import { watch } from "../watch";
import { watchEffect } from "../watchEffect"; import { watchEffect } from "../watchEffect";
describe("watch", () => { describe("watch", () => {
it("watch immediate", () => { it("watch immediate", () => {
const store = deepSignal({ const store = deepSignal({
userinfo: { userinfo: {
name: "tom", name: "tom",
}, },
});
let val!: string;
watch(
store,
({ newValue }) => {
val = newValue.userinfo.name;
},
{ immediate: true }
);
expect(val).toEqual("tom");
}); });
let val!: string; it("watch deep", () => {
watch( const store = deepSignal({
store, userinfo: {
({ newValue }) => { name: "tom",
val = newValue.userinfo.name; },
}, });
{ immediate: true } let val!: string;
); watch(
expect(val).toEqual("tom"); store,
}); ({ newValue }) => {
it("watch deep", () => { val = newValue.userinfo.name;
const store = deepSignal({ },
userinfo: { { immediate: true }
name: "tom", );
}, 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", () => { it("watch once", () => {
const store = deepSignal({ const store = deepSignal({
userinfo: { userinfo: {
name: "tom", 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"); it("watch effect", () => {
store.userinfo.name = "jon"; const store = deepSignal({
// once watcher shouldn't update after first run userinfo: {
expect(val).toEqual("tom"); name: "tom",
}); },
});
let x = undefined;
watchEffect(() => {
x = store.userinfo.name;
});
it("watch effect", () => { expect(x).toEqual("tom");
const store = deepSignal({ store.userinfo.name = "jon";
userinfo: { expect(x).toEqual("jon");
name: "tom",
},
}); });
let x = undefined;
watchEffect(() => {
x = store.userinfo.name;
});
expect(x).toEqual("tom");
store.userinfo.name = "jon";
expect(x).toEqual("jon");
});
}); });

@ -1,10 +1,10 @@
import { isObject, isPlainObject, isSet, isMap, isArray } from "./utils"; import { isObject, isPlainObject, isSet, isMap, isArray } from "./utils";
import { isSignal } from "./core"; import { isSignal } from "./core";
import { import {
isDeepSignal, isDeepSignal,
subscribeDeepMutations, subscribeDeepMutations,
getDeepSignalRootId, getDeepSignalRootId,
DeepPatch, DeepPatch,
} from "./deepSignal"; } from "./deepSignal";
import { ReactiveFlags } from "./contents"; import { ReactiveFlags } from "./contents";
@ -15,125 +15,125 @@ export type WatchEffect = (registerCleanup: RegisterCleanup) => void;
/** Options for {@link watch}. */ /** Options for {@link watch}. */
export interface WatchOptions { export interface WatchOptions {
/** Trigger the callback immediately with the current value (default: false). */ /** Trigger the callback immediately with the current value (default: false). */
immediate?: boolean; immediate?: boolean;
/** Auto-stop the watcher after the first callback run that delivers patches (or immediate call if no patches). */ /** Auto-stop the watcher after the first callback run that delivers patches (or immediate call if no patches). */
once?: boolean; once?: boolean;
/** Allow legacy/unknown options (ignored) to avoid hard breaks while migrating. */ /** Allow legacy/unknown options (ignored) to avoid hard breaks while migrating. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
[legacy: string]: any; [legacy: string]: any;
} }
export interface WatchPatchEvent<Root = any> { export interface WatchPatchEvent<Root = any> {
/** Patch batch that triggered this callback (may be empty for immediate). */ /** Patch batch that triggered this callback (may be empty for immediate). */
patches: DeepPatch[]; patches: DeepPatch[];
/** Previous snapshot (deep-cloned) of the root value before these patches. Undefined on first call. */ /** Previous snapshot (deep-cloned) of the root value before these patches. Undefined on first call. */
oldValue: Root | undefined; oldValue: Root | undefined;
/** Current root value (live proxy). */ /** Current root value (live proxy). */
newValue: Root; newValue: Root;
} }
export type WatchPatchCallback<Root = any> = ( export type WatchPatchCallback<Root = any> = (
event: WatchPatchEvent<Root> event: WatchPatchEvent<Root>
) => any; ) => any;
// Internal helper kept for external compatibility. // Internal helper kept for external compatibility.
export const remove = <T>(arr: T[], el: T): void => { export const remove = <T>(arr: T[], el: T): void => {
const i = arr.indexOf(el); const i = arr.indexOf(el);
if (i > -1) arr.splice(i, 1); if (i > -1) arr.splice(i, 1);
}; };
/** Observe patch batches on a deep signal root. */ /** Observe patch batches on a deep signal root. */
export function watch<Root = any>( export function watch<Root = any>(
source: Root, source: Root,
callback: WatchPatchCallback<Root>, callback: WatchPatchCallback<Root>,
options: WatchOptions = {} options: WatchOptions = {}
) { ) {
if (!isDeepSignal(source)) { if (!isDeepSignal(source)) {
throw new Error( throw new Error(
"watch() now only supports deepSignal roots (patch mode only)" "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;
}
} }
}; const { immediate, once } = options;
// Deep clone snapshot helper (JSON clone sufficient for typical reactive plain data) const rootId = getDeepSignalRootId(source as any)!;
const clone = (v: any) => {
try { let active = true;
return JSON.parse(JSON.stringify(v)); let cleanup: (() => void) | undefined;
} catch { const registerCleanup: RegisterCleanup = (fn) => {
return undefined as any; cleanup = fn;
} };
}; const runCleanup = () => {
let lastSnapshot: Root | undefined = clone(source); if (cleanup) {
try {
const stopListening = () => { cleanup();
if (!active) return; } catch {
active = false; /* ignore */
runCleanup(); } finally {
unsubscribe && unsubscribe(); cleanup = undefined;
}; }
}
const deliver = (patches: DeepPatch[]) => { };
if (!active) return;
runCleanup(); // Deep clone snapshot helper (JSON clone sufficient for typical reactive plain data)
const prev = lastSnapshot; const clone = (v: any) => {
const next = source as any as Root; // live proxy try {
try { return JSON.parse(JSON.stringify(v));
callback({ } catch {
patches, return undefined as any;
oldValue: prev, }
newValue: next, };
}); let lastSnapshot: Root | undefined = clone(source);
} finally {
if (active) lastSnapshot = clone(next); const stopListening = () => {
if (once) 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([]);
} }
};
return {
const unsubscribe = subscribeDeepMutations(rootId, (patches) => { /** Stop listening to future patch batches; idempotent. */
if (!patches.length) return; // ignore empty batches stopListening,
deliver(patches); /** Register a cleanup callback run before the next invocation / stop. */
}); registerCleanup,
};
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,
};
} }
// observe alias // observe alias
export function observe( export function observe(
source: any, source: any,
cb: WatchPatchCallback, cb: WatchPatchCallback,
options?: WatchOptions options?: WatchOptions
) { ) {
return watch(source, cb, options); return watch(source, cb, options);
} }
// Instrumentation counter for performance tests (number of traverse invocations) // 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 export let __traverseCount = 0; // retained for external tooling/tests although watch no longer uses traversal
/** Reset the traversal instrumentation counter back to 0. */ /** Reset the traversal instrumentation counter back to 0. */
export function __resetTraverseCount() { 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. * Depth-limited; protects against cycles via `seen` set; respects ReactiveFlags.SKIP opt-out.
*/ */
export function traverse( export function traverse(
value: unknown, value: unknown,
depth: number = Infinity, depth: number = Infinity,
seen?: Set<unknown> seen?: Set<unknown>
): unknown { ): unknown {
__traverseCount++; __traverseCount++;
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) { if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value; 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);
} }
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => { seen = seen || new Set();
traverse(v, depth, seen); if (seen.has(value)) {
}); return value;
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], depth, seen);
} }
for (const key of Object.getOwnPropertySymbols(value)) { seen.add(value);
if (Object.prototype.propertyIsEnumerable.call(value, key)) { depth--;
traverse(value[key as any], depth, seen); 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;
} }

Loading…
Cancel
Save