diff --git a/src/deepSignal.ts b/src/deepSignal.ts index f70a396..0d90548 100644 --- a/src/deepSignal.ts +++ b/src/deepSignal.ts @@ -371,7 +371,11 @@ const createProxy = ( }; // Set-specific access & structural patch emission. -function getFromSet(raw: Set, key: string, receiver: object): any { +function getFromSet( + raw: Set, + key: string | symbol, + receiver: object +): any { const meta = proxyMeta.get(receiver); // Helper to proxy a single entry (object) & assign synthetic id if needed. const ensureEntryProxy = (entry: any) => { @@ -516,8 +520,27 @@ function getFromSet(raw: Set, key: string, receiver: object): any { }); }; } + // Properly handle native iteration (for..of, Array.from, spread) by binding to the raw Set. + if (key === Symbol.iterator) { + // Return a function whose `this` is the raw Set (avoids brand check failure on the proxy). + return function (this: any) { + // Use raw.values() so we can still ensure child entries are proxied lazily. + const iterable = raw.values(); + return { + [Symbol.iterator]() { + return this; + }, + next() { + const n = iterable.next(); + if (n.done) return n; + const entry = ensureEntryProxy(n.value); + return { value: entry, done: false }; + }, + } as Iterator; + }; + } if (key === Symbol.iterator.toString()) { - // string form access of iterator symbol; pass through + // string form access of iterator symbol; pass through (rare path) } const val = (raw as any)[key]; if (typeof val === "function") return val.bind(raw); @@ -601,8 +624,8 @@ const get = (target: object, fullKey: string, receiver: object): unknown => { if (peeking) return Reflect.get(target, fullKey, receiver); // Set handling delegated completely. - if (target instanceof Set && typeof fullKey === "string") { - return getFromSet(target as Set, fullKey, receiver); + if (target instanceof Set) { + return getFromSet(target as Set, fullKey as any, receiver); } const norm = normalizeKey(target, fullKey, isArrayMeta, receiver); if ((norm as any).shortCircuit) return (norm as any).shortCircuit; // returned meta proxy diff --git a/src/test/watchPatches.test.ts b/src/test/watchPatches.test.ts index 0569b77..8d2d067 100644 --- a/src/test/watchPatches.test.ts +++ b/src/test/watchPatches.test.ts @@ -298,6 +298,27 @@ describe("watch (patch mode)", () => { expect(new Set(keys).size).toBe(2); stop(); }); + + it("allows Array.from() and spread on Set without brand errors and tracks nested mutation", async () => { + const st = deepSignal({ + s: new Set([{ id: "eIter", inner: { v: 1 } }]), + }); + // Regression: previously 'values method called on incompatible Proxy' was thrown here. + const arr = Array.from(st.s); + expect(arr.length).toBe(1); + expect(arr[0].inner.v).toBe(1); + const spread = [...st.s]; + expect(spread[0].inner.v).toBe(1); + const batches: DeepPatch[][] = []; + const { stopListening: stop } = watch(st, ({ patches }) => + batches.push(patches) + ); + spread[0].inner.v = 2; // mutate nested field of iterated (proxied) entry + await Promise.resolve(); + const flat = batches.flat().map((p) => p.path.join(".")); + expect(flat.some((p) => p.endsWith("eIter.inner.v"))).toBe(true); + stop(); + }); }); describe("Arrays & mixed batch", () => {