From afeb50860247c1c318d2cecfd6aee45d3c8018b0 Mon Sep 17 00:00:00 2001 From: Brenley Dueck Date: Thu, 30 Apr 2026 16:48:19 -0500 Subject: [PATCH] Fix proxy invariant crash when produce draft is returned from a getter (#2668) setterTraps.get in store/modifiers now bypasses internal store symbols ($PROXY, $TRACK, $NODE, $HAS) and __proto__, mirroring the main store proxy traps. Previously, reading $PROXY through the produce draft proxy returned a freshly wrapped draft instead of the underlying store proxy, violating the non-configurable data property invariant on the raw target. --- packages/solid/store/src/modifiers.ts | 11 ++++++++- packages/solid/store/test/modifiers.spec.ts | 27 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/solid/store/src/modifiers.ts b/packages/solid/store/src/modifiers.ts index 3f4cd3871..d05439d8f 100644 --- a/packages/solid/store/src/modifiers.ts +++ b/packages/solid/store/src/modifiers.ts @@ -1,4 +1,5 @@ -import { setProperty, unwrap, isWrappable, StoreNode, $RAW } from "./store.js"; +import { $PROXY, $TRACK } from "solid-js"; +import { setProperty, unwrap, isWrappable, StoreNode, $RAW, $NODE, $HAS } from "./store.js"; const $ROOT = Symbol("store-root"); @@ -144,6 +145,14 @@ const setterTraps: ProxyHandler = { get(target, property): any { if (property === $RAW) return target; const value = target[property]; + if ( + property === $PROXY || + property === $TRACK || + property === $NODE || + property === $HAS || + property === "__proto__" + ) + return value; let proxy; return isWrappable(value) ? producers.get(value) || diff --git a/packages/solid/store/test/modifiers.spec.ts b/packages/solid/store/test/modifiers.spec.ts index 2ca8592f0..1accf696d 100644 --- a/packages/solid/store/test/modifiers.spec.ts +++ b/packages/solid/store/test/modifiers.spec.ts @@ -280,6 +280,33 @@ describe("setState with produce", () => { expect(state[1].done).toBe(true); expect(state[2].title).toBe("Go Home"); }); + + test("Leaked draft proxy returned from getter does not violate proxy invariant (#2668)", () => { + let leaked: any; + const [state, setState] = createStore<{ items: number[]; readonly probe: number[] }>({ + items: [], + get probe() { + // touch items so it's tracked, then return leaked draft + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + this.items; + return leaked; + } + }); + expect(() => { + setState( + produce(draft => { + leaked = draft.items; + // Reading probe through the store proxy returns the leaked draft. + // Previously this triggered a "TypeError: 'get' on proxy: property + // 'Symbol(solid-proxy)' is a read-only and non-configurable data + // property on the proxy target but the proxy did not return its + // actual value" because setterTraps.get wrapped the $PROXY value. + state.probe; + }) + ); + }).not.toThrow(); + expect(Array.isArray(state.items)).toBe(true); + }); }); describe("modifyMutable with reconcile", () => {