Skip to content

Commit

Permalink
fix in introspection for stores
Browse files Browse the repository at this point in the history
  • Loading branch information
ryansolid committed Aug 3, 2023
1 parent 23f3c7a commit 6c9879c
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .changeset/new-cobras-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"solid-js": patch
---

fix in introspection in stores
14 changes: 8 additions & 6 deletions packages/solid/store/src/mutable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -40,16 +41,16 @@ const proxyTraps: ProxyHandler<StoreNode> = {
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));
Expand All @@ -64,10 +65,11 @@ const proxyTraps: ProxyHandler<StoreNode> = {
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;
},

Expand Down
57 changes: 29 additions & 28 deletions packages/solid/store/src/store.ts
Original file line number Diff line number Diff line change
@@ -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 } = {
Expand Down Expand Up @@ -114,15 +115,21 @@ export function unwrap<T>(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<any>(value, {
equals: false,
internal: true
});
(s as DataNode).$ = set;
return (nodes[property] = s as DataNode);
}

export function proxyDescriptor(target: StoreNode, property: PropertyKey) {
Expand All @@ -136,26 +143,14 @@ 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) {
trackSelf(target);
return Reflect.ownKeys(target);
}

function createDataNode(value?: any) {
const [s, set] = createSignal<any>(value, {
equals: false,
internal: true
});
(s as DataNode).$ = set;
return s as DataNode;
}

const proxyTraps: ProxyHandler<StoreNode> = {
get(target, property, receiver) {
if (property === $RAW) return target;
Expand All @@ -164,10 +159,10 @@ const proxyTraps: ProxyHandler<StoreNode> = {
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);
Expand All @@ -176,7 +171,7 @@ const proxyTraps: ProxyHandler<StoreNode> = {
(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;
},
Expand All @@ -187,10 +182,11 @@ const proxyTraps: ProxyHandler<StoreNode> = {
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;
},

Expand Down Expand Up @@ -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.$();
}
Expand Down
49 changes: 49 additions & 0 deletions packages/solid/store/test/mutable.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
49 changes: 49 additions & 0 deletions packages/solid/store/test/store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 6c9879c

Please sign in to comment.