fix set iterator (destructuring etc. form)

main
Laurin Weger 3 weeks ago
parent 400ba719b5
commit a04162b724
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 31
      src/deepSignal.ts
  2. 21
      src/test/watchPatches.test.ts

@ -371,7 +371,11 @@ const createProxy = (
}; };
// Set-specific access & structural patch emission. // Set-specific access & structural patch emission.
function getFromSet(raw: Set<any>, key: string, receiver: object): any { function getFromSet(
raw: Set<any>,
key: string | symbol,
receiver: object
): any {
const meta = proxyMeta.get(receiver); const meta = proxyMeta.get(receiver);
// Helper to proxy a single entry (object) & assign synthetic id if needed. // Helper to proxy a single entry (object) & assign synthetic id if needed.
const ensureEntryProxy = (entry: any) => { const ensureEntryProxy = (entry: any) => {
@ -516,8 +520,27 @@ function getFromSet(raw: Set<any>, 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<any>;
};
}
if (key === Symbol.iterator.toString()) { 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]; const val = (raw as any)[key];
if (typeof val === "function") return val.bind(raw); if (typeof val === "function") return val.bind(raw);
@ -601,8 +624,8 @@ const get =
(target: object, fullKey: string, receiver: object): unknown => { (target: object, fullKey: string, receiver: object): unknown => {
if (peeking) return Reflect.get(target, fullKey, receiver); if (peeking) return Reflect.get(target, fullKey, receiver);
// Set handling delegated completely. // Set handling delegated completely.
if (target instanceof Set && typeof fullKey === "string") { if (target instanceof Set) {
return getFromSet(target as Set<any>, fullKey, receiver); return getFromSet(target as Set<any>, fullKey as any, receiver);
} }
const norm = normalizeKey(target, fullKey, isArrayMeta, receiver); const norm = normalizeKey(target, fullKey, isArrayMeta, receiver);
if ((norm as any).shortCircuit) return (norm as any).shortCircuit; // returned meta proxy if ((norm as any).shortCircuit) return (norm as any).shortCircuit; // returned meta proxy

@ -298,6 +298,27 @@ describe("watch (patch mode)", () => {
expect(new Set(keys).size).toBe(2); expect(new Set(keys).size).toBe(2);
stop(); stop();
}); });
it("allows Array.from() and spread on Set without brand errors and tracks nested mutation", async () => {
const st = deepSignal({
s: new Set<any>([{ 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", () => { describe("Arrays & mixed batch", () => {

Loading…
Cancel
Save