Skip to content

Commit

Permalink
DEV: Add a way to observe stores (#1307)
Browse files Browse the repository at this point in the history
* dev: Add a way to observe stores, export DEV object

Adds a dev-only ability to observe stores by adding a new `$ON_UPDATE` symbol.
Add off the dev APIs are bundled into DEV object, as in core module
Some typings got impored too
Added a jsdoc comment to `unwrap` as well

* Correct changed produce types

* Change the OnStoreNodeUpdate arguments

* Change to global listener for stoes, add test
  • Loading branch information
thetarnav authored Nov 11, 2022
1 parent 5dac82e commit 40d6db6
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 21 deletions.
4 changes: 2 additions & 2 deletions packages/solid/src/reactive/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ let rootCount = 0;
const [transPending, setTransPending] = /*@__PURE__*/ createSignal(false);

declare global {
var _$afterUpdate: () => void;
var _$afterCreateRoot: (root: Owner) => void;
var _$afterUpdate: (() => void) | undefined;
var _$afterCreateRoot: ((root: Owner) => void) | undefined;
}

export interface SignalState<T> {
Expand Down
4 changes: 4 additions & 0 deletions packages/solid/store/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ export type {
} from "./store.js";
export * from "./mutable.js";
export * from "./modifiers.js";

// dev
import { $NAME, $NODE, isWrappable } from "./store.js";
export const DEV = "_SOLID_DEV_" ? ({ $NAME, $NODE, isWrappable } as const) : undefined;
4 changes: 2 additions & 2 deletions packages/solid/store/src/modifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export function reconcile<T extends U, U>(
return state => {
if (!isWrappable(state) || !isWrappable(v)) return v;
const res = applyState(v, { [$ROOT]: state }, $ROOT, merge, key);
return res === undefined ? state as T : res as T;
return res === undefined ? (state as T) : res;
};
}

Expand Down Expand Up @@ -152,7 +152,7 @@ export function produce<T>(fn: (state: T) => void): (state: T) => T {
if (!(proxy = producers.get(state as Record<keyof T, T[keyof T]>))) {
producers.set(
state as Record<keyof T, T[keyof T]>,
(proxy = new Proxy(state, setterTraps))
(proxy = new Proxy(state as Extract<T, object>, setterTraps))
);
}
fn(proxy);
Expand Down
2 changes: 2 additions & 0 deletions packages/solid/store/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,5 @@ export function produce<T>(fn: (state: T) => void): (state: T) => T {
return state;
};
}

export const DEV = undefined;
70 changes: 54 additions & 16 deletions packages/solid/store/src/store.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
import { getListener, batch, DEV, $PROXY, $TRACK, Accessor, createSignal } from "solid-js";
import { getListener, batch, DEV, $PROXY, $TRACK, createSignal } from "solid-js";

export const $RAW = Symbol("store-raw"),
$NODE = Symbol("store-node"),
$NAME = Symbol("store-name");

export type StoreNode = Record<PropertyKey, any>;
// dev
declare global {
var _$onStoreNodeUpdate: OnStoreNodeUpdate | undefined;
}

type DataNode = {
(): any;
$(value?: any): void;
};
type DataNodes = Record<PropertyKey, DataNode>;

export type OnStoreNodeUpdate = (
state: StoreNode,
property: PropertyKey,
value: StoreNode | NotWrappable,
prev: StoreNode | NotWrappable
) => void;

export interface StoreNode {
[$NAME]?: string;
[$NODE]?: DataNodes;
[key: PropertyKey]: any;
}

export namespace SolidStore {
export interface Unwrappable {}
}
Expand Down Expand Up @@ -55,6 +79,17 @@ export function isWrappable(obj: any) {
);
}

/**
* Returns the underlying data in the store without a proxy.
* @param item store proxy object
* @example
* ```js
* const initial = {z...};
* const [state, setState] = createStore(initial);
* initial === state; // => false
* initial === unwrap(state); // => true
* ```
*/
export function unwrap<T>(item: T, set?: Set<unknown>): T;
export function unwrap<T>(item: any, set = new Set()): T {
let result, unwrapped, v, prop;
Expand All @@ -75,22 +110,22 @@ export function unwrap<T>(item: any, set = new Set()): T {
desc = Object.getOwnPropertyDescriptors(item);
for (let i = 0, l = keys.length; i < l; i++) {
prop = keys[i];
if ((desc as any)[prop].get) continue;
if (desc[prop].get) continue;
v = item[prop];
if ((unwrapped = unwrap(v, set)) !== v) item[prop] = unwrapped;
}
}
return item;
}

export function getDataNodes(target: StoreNode) {
export function getDataNodes(target: StoreNode): DataNodes {
let nodes = target[$NODE];
if (!nodes) Object.defineProperty(target, $NODE, { value: (nodes = {}) });
return nodes;
}

export function getDataNode(nodes: Record<string, any>, property: string | symbol, value: any) {
return nodes[property as string] || (nodes[property as string] = createDataNode(value));
export function getDataNode(nodes: DataNodes, property: PropertyKey, value: any) {
return nodes[property] || (nodes[property] = createDataNode(value));
}

export function proxyDescriptor(target: StoreNode, property: PropertyKey) {
Expand Down Expand Up @@ -127,8 +162,8 @@ function createDataNode(value?: any) {
equals: false,
internal: true
});
(s as Accessor<any> & { $: (v: any) => void }).$ = set;
return s as Accessor<any> & { $: (v: any) => void };
(s as DataNode).$ = set;
return s as DataNode;
}

const proxyTraps: ProxyHandler<StoreNode> = {
Expand Down Expand Up @@ -192,16 +227,19 @@ export function setProperty(
property: PropertyKey,
value: any,
deleting: boolean = false
) {
): void {
if (!deleting && state[property] === value) return;
const prev = state[property];
const len = state.length;
if (value === undefined) {
delete state[property];
} else state[property] = value;
const prev = state[property],
len = state.length;

if ("_SOLID_DEV_" && globalThis._$onStoreNodeUpdate)
globalThis._$onStoreNodeUpdate(state, property, value, prev);

if (value === undefined) delete state[property];
else state[property] = value;
let nodes = getDataNodes(state),
node;
if ((node = getDataNode(nodes, property as string, prev))) node.$(() => value);
node: DataNode;
if ((node = getDataNode(nodes, property, prev))) node.$(() => value);

if (Array.isArray(state) && state.length !== len)
(node = getDataNode(nodes, "length", len)) && node.$(state.length);
Expand Down
15 changes: 14 additions & 1 deletion packages/solid/test/dev.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Owner,
createContext
} from "../src";
import { createStore } from "../store/src";
import { createStore, unwrap } from "../store/src";

describe("Dev features", () => {
test("Reactive graph serialization", () => {
Expand Down Expand Up @@ -122,4 +122,17 @@ describe("Dev features", () => {
});
});
});

test("OnStoreNodeUpdate Hook", () => {
const cb = jest.fn();
global._$onStoreNodeUpdate = cb;
const [s, set] = createStore({ firstName: "John", lastName: "Smith", inner: { foo: 1 } });
expect(cb).toHaveBeenCalledTimes(0);
set({ firstName: "Matt" });
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith(unwrap(s), "firstName", "Matt", "John");
set("inner", "foo", 2);
expect(cb).toHaveBeenCalledTimes(2);
expect(cb).toHaveBeenCalledWith(unwrap(s.inner), "foo", 2, 1);
});
});

0 comments on commit 40d6db6

Please sign in to comment.