Skip to content

Commit afeb508

Browse files
committed
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.
1 parent 878f94a commit afeb508

2 files changed

Lines changed: 37 additions & 1 deletion

File tree

packages/solid/store/src/modifiers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { setProperty, unwrap, isWrappable, StoreNode, $RAW } from "./store.js";
1+
import { $PROXY, $TRACK } from "solid-js";
2+
import { setProperty, unwrap, isWrappable, StoreNode, $RAW, $NODE, $HAS } from "./store.js";
23

34
const $ROOT = Symbol("store-root");
45

@@ -144,6 +145,14 @@ const setterTraps: ProxyHandler<StoreNode> = {
144145
get(target, property): any {
145146
if (property === $RAW) return target;
146147
const value = target[property];
148+
if (
149+
property === $PROXY ||
150+
property === $TRACK ||
151+
property === $NODE ||
152+
property === $HAS ||
153+
property === "__proto__"
154+
)
155+
return value;
147156
let proxy;
148157
return isWrappable(value)
149158
? producers.get(value) ||

packages/solid/store/test/modifiers.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,33 @@ describe("setState with produce", () => {
280280
expect(state[1].done).toBe(true);
281281
expect(state[2].title).toBe("Go Home");
282282
});
283+
284+
test("Leaked draft proxy returned from getter does not violate proxy invariant (#2668)", () => {
285+
let leaked: any;
286+
const [state, setState] = createStore<{ items: number[]; readonly probe: number[] }>({
287+
items: [],
288+
get probe() {
289+
// touch items so it's tracked, then return leaked draft
290+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
291+
this.items;
292+
return leaked;
293+
}
294+
});
295+
expect(() => {
296+
setState(
297+
produce(draft => {
298+
leaked = draft.items;
299+
// Reading probe through the store proxy returns the leaked draft.
300+
// Previously this triggered a "TypeError: 'get' on proxy: property
301+
// 'Symbol(solid-proxy)' is a read-only and non-configurable data
302+
// property on the proxy target but the proxy did not return its
303+
// actual value" because setterTraps.get wrapped the $PROXY value.
304+
state.probe;
305+
})
306+
);
307+
}).not.toThrow();
308+
expect(Array.isArray(state.items)).toBe(true);
309+
});
283310
});
284311

285312
describe("modifyMutable with reconcile", () => {

0 commit comments

Comments
 (0)