diff --git a/.changeset/new-cobras-chew.md b/.changeset/new-cobras-chew.md new file mode 100644 index 000000000..c507b9da7 --- /dev/null +++ b/.changeset/new-cobras-chew.md @@ -0,0 +1,5 @@ +--- +"solid-js": patch +--- + +fix in introspection in stores diff --git a/packages/solid/store/src/mutable.ts b/packages/solid/store/src/mutable.ts index 4f564d845..63e92b93f 100644 --- a/packages/solid/store/src/mutable.ts +++ b/packages/solid/store/src/mutable.ts @@ -2,11 +2,12 @@ import { batch, getListener, DEV, $PROXY, $TRACK } from "solid-js"; import { unwrap, isWrappable, - getDataNodes, + getNodes, trackSelf, - getDataNode, + getNode, $RAW, $NODE, + $HAS, StoreNode, setProperty, ownKeys @@ -40,16 +41,16 @@ const proxyTraps: ProxyHandler = { trackSelf(target); return receiver; } - const nodes = getDataNodes(target); + const nodes = getNodes(target, $NODE); const tracked = nodes[property]; let value = tracked ? tracked() : target[property]; - if (property === $NODE || property === "__proto__") return value; + if (property === $NODE || property === $HAS || property === "__proto__") return value; if (!tracked) { const desc = Object.getOwnPropertyDescriptor(target, property); const isFunction = typeof value === "function"; if (getListener() && (!isFunction || target.hasOwnProperty(property)) && !(desc && desc.get)) - value = getDataNode(nodes, property, value)(); + value = getNode(nodes, property, value)(); else if (value != null && isFunction && value === Array.prototype[property as any]) { return (...args: unknown[]) => batch(() => Array.prototype[property as any].apply(receiver, args)); @@ -64,10 +65,11 @@ const proxyTraps: ProxyHandler = { property === $PROXY || property === $TRACK || property === $NODE || + property === $HAS || property === "__proto__" ) return true; - this.get!(target, property, target); + getListener() && getNode(getNodes(target, $HAS), property)(); return property in target; }, diff --git a/packages/solid/store/src/store.ts b/packages/solid/store/src/store.ts index d7481b727..92084ac60 100644 --- a/packages/solid/store/src/store.ts +++ b/packages/solid/store/src/store.ts @@ -1,7 +1,8 @@ import { getListener, batch, DEV, $PROXY, $TRACK, createSignal } from "solid-js"; export const $RAW = Symbol("store-raw"), - $NODE = Symbol("store-node"); + $NODE = Symbol("store-node"), + $HAS = Symbol("store-has"); // debug hooks for devtools export const DevHooks: { onStoreNodeUpdate: OnStoreNodeUpdate | null } = { @@ -114,15 +115,21 @@ export function unwrap(item: any, set = new Set()): T { return item; } -export function getDataNodes(target: StoreNode): DataNodes { - let nodes = target[$NODE]; +export function getNodes(target: StoreNode, symbol: typeof $NODE | typeof $HAS): DataNodes { + let nodes = target[symbol]; if (!nodes) - Object.defineProperty(target, $NODE, { value: (nodes = Object.create(null) as DataNodes) }); + Object.defineProperty(target, symbol, { value: (nodes = Object.create(null) as DataNodes) }); return nodes; } -export function getDataNode(nodes: DataNodes, property: PropertyKey, value: any) { - return nodes[property] || (nodes[property] = createDataNode(value)); +export function getNode(nodes: DataNodes, property: PropertyKey, value?: any) { + if (nodes[property]) return nodes[property]!; + const [s, set] = createSignal(value, { + equals: false, + internal: true + }); + (s as DataNode).$ = set; + return (nodes[property] = s as DataNode); } export function proxyDescriptor(target: StoreNode, property: PropertyKey) { @@ -136,10 +143,7 @@ export function proxyDescriptor(target: StoreNode, property: PropertyKey) { } export function trackSelf(target: StoreNode) { - if (getListener()) { - const nodes = getDataNodes(target); - (nodes._ || (nodes._ = createDataNode()))(); - } + getListener() && getNode(getNodes(target, $NODE), "_")(); } export function ownKeys(target: StoreNode) { @@ -147,15 +151,6 @@ export function ownKeys(target: StoreNode) { return Reflect.ownKeys(target); } -function createDataNode(value?: any) { - const [s, set] = createSignal(value, { - equals: false, - internal: true - }); - (s as DataNode).$ = set; - return s as DataNode; -} - const proxyTraps: ProxyHandler = { get(target, property, receiver) { if (property === $RAW) return target; @@ -164,10 +159,10 @@ const proxyTraps: ProxyHandler = { trackSelf(target); return receiver; } - const nodes = getDataNodes(target); + const nodes = getNodes(target, $NODE); const tracked = nodes[property]; let value = tracked ? tracked() : target[property]; - if (property === $NODE || property === "__proto__") return value; + if (property === $NODE || property === $HAS || property === "__proto__") return value; if (!tracked) { const desc = Object.getOwnPropertyDescriptor(target, property); @@ -176,7 +171,7 @@ const proxyTraps: ProxyHandler = { (typeof value !== "function" || target.hasOwnProperty(property)) && !(desc && desc.get) ) - value = getDataNode(nodes, property, value)(); + value = getNode(nodes, property, value)(); } return isWrappable(value) ? wrap(value) : value; }, @@ -187,10 +182,11 @@ const proxyTraps: ProxyHandler = { property === $PROXY || property === $TRACK || property === $NODE || + property === $HAS || property === "__proto__" ) return true; - this.get!(target, property, target); + getListener() && getNode(getNodes(target, $HAS), property)(); return property in target; }, @@ -222,15 +218,20 @@ export function setProperty( if ("_SOLID_DEV_") DevHooks.onStoreNodeUpdate && DevHooks.onStoreNodeUpdate(state, property, value, prev); - if (value === undefined) delete state[property]; - else state[property] = value; - let nodes = getDataNodes(state), + if (value === undefined) { + delete state[property]; + if (state[$HAS] && state[$HAS][property] && prev !== undefined) state[$HAS][property].$(); + } else { + state[property] = value; + if (state[$HAS] && state[$HAS][property] && prev === undefined) state[$HAS][property].$(); + } + let nodes = getNodes(state, $NODE), node: DataNode | undefined; - if ((node = getDataNode(nodes, property, prev))) node.$(() => value); + if ((node = getNode(nodes, property, prev))) node.$(() => value); if (Array.isArray(state) && state.length !== len) { for (let i = state.length; i < len; i++) (node = nodes[i]) && node.$(); - (node = getDataNode(nodes, "length", len)) && node.$(state.length); + (node = getNode(nodes, "length", len)) && node.$(state.length); } (node = nodes._) && node.$(); } diff --git a/packages/solid/store/test/mutable.spec.ts b/packages/solid/store/test/mutable.spec.ts index 5c3eba41a..b8946dee7 100644 --- a/packages/solid/store/test/mutable.spec.ts +++ b/packages/solid/store/test/mutable.spec.ts @@ -267,3 +267,52 @@ describe("State wrapping", () => { expect(state).toEqual([2, 1, 3]); }); }); + +describe("In Operator", () => { + test("wrapped nested class", () => { + let access = 0; + const store = createMutable<{ a?: number; b?: number; c?: number }>({ + a: 1, + get b() { + access++; + return 2; + } + }); + + expect("a" in store).toBe(true); + expect("b" in store).toBe(true); + expect("c" in store).toBe(false); + expect(access).toBe(0); + + const [a, b, c] = createRoot(() => { + return [ + createMemo(() => "a" in store), + createMemo(() => "b" in store), + createMemo(() => "c" in store) + ]; + }); + + expect(a()).toBe(true); + expect(b()).toBe(true); + expect(c()).toBe(false); + expect(access).toBe(0); + + store.c = 3; + + expect(a()).toBe(true); + expect(b()).toBe(true); + expect(c()).toBe(true); + expect(access).toBe(0); + + delete store.a; + expect(a()).toBe(false); + expect(b()).toBe(true); + expect(c()).toBe(true); + expect(access).toBe(0); + + expect("a" in store).toBe(false); + expect("b" in store).toBe(true); + expect("c" in store).toBe(true); + expect(access).toBe(0); + }); +}); diff --git a/packages/solid/store/test/store.spec.ts b/packages/solid/store/test/store.spec.ts index 8f60fee37..0e2df2309 100644 --- a/packages/solid/store/test/store.spec.ts +++ b/packages/solid/store/test/store.spec.ts @@ -749,6 +749,55 @@ describe("Nested Classes", () => { }); }); +describe("In Operator", () => { + test("wrapped nested class", () => { + let access = 0; + const [store, setStore] = createStore<{ a?: number; b?: number; c?: number }>({ + a: 1, + get b() { + access++; + return 2; + } + }); + + expect("a" in store).toBe(true); + expect("b" in store).toBe(true); + expect("c" in store).toBe(false); + expect(access).toBe(0); + + const [a, b, c] = createRoot(() => { + return [ + createMemo(() => "a" in store), + createMemo(() => "b" in store), + createMemo(() => "c" in store) + ]; + }); + + expect(a()).toBe(true); + expect(b()).toBe(true); + expect(c()).toBe(false); + expect(access).toBe(0); + + setStore("c", 3); + + expect(a()).toBe(true); + expect(b()).toBe(true); + expect(c()).toBe(true); + expect(access).toBe(0); + + setStore("a", undefined); + expect(a()).toBe(false); + expect(b()).toBe(true); + expect(c()).toBe(true); + expect(access).toBe(0); + + expect("a" in store).toBe(false); + expect("b" in store).toBe(true); + expect("c" in store).toBe(true); + expect(access).toBe(0); + }); +}); + // type tests // NotWrappable keys are ignored