@solid-primitives/deep 0.3.6 → 1.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,10 @@
4
4
 
5
5
  # @solid-primitives/deep
6
6
 
7
- [![size](https://img.shields.io/bundlephobia/minzip/@solid-primitives/deep?style=for-the-badge&label=size)](https://bundlephobia.com/package/@solid-primitives/deep)
7
+ [![size](https://img.shields.io/badge/size-1.3_kB-blue?style=for-the-badge)](https://bundlephobia.com/package/@solid-primitives/deep)
8
8
  [![version](https://img.shields.io/npm/v/@solid-primitives/deep?style=for-the-badge)](https://www.npmjs.com/package/@solid-primitives/deep)
9
9
  [![stage](https://img.shields.io/endpoint?style=for-the-badge&url=https%3A%2F%2Fraw.githubusercontent.com%2Fsolidjs-community%2Fsolid-primitives%2Fmain%2Fassets%2Fbadges%2Fstage-1.json)](https://github.com/solidjs-community/solid-primitives#contribution-process)
10
+ [![tested with vitest](https://img.shields.io/badge/tested_with-vitest-6E9F18?style=for-the-badge&logo=vitest)](https://vitest.dev)
10
11
 
11
12
  Primitives for tracking and observing nested reactive objects in Solid.
12
13
 
@@ -14,6 +15,35 @@ Primitives for tracking and observing nested reactive objects in Solid.
14
15
  - [`trackStore`](#trackstore) - A more performant alternative to `trackDeep` utilizing specific store implementations.
15
16
  - [`captureStoreUpdates`](#capturestoreupdates) - A utility function that captures all updates to a store and returns them as an array.
16
17
 
18
+ ## Comparison with Solid's built-in `deep`
19
+
20
+ Solid 2.0 ships a `deep` helper in `solid-js` that tracks all nested properties of a store and returns a **plain snapshot** — a non-reactive copy suitable for serialization:
21
+
22
+ ```ts
23
+ import { deep } from "solid-js";
24
+
25
+ createEffect(
26
+ () => deep(store),
27
+ snapshot => localStorage.setItem("state", JSON.stringify(snapshot)),
28
+ );
29
+ ```
30
+
31
+ This package complements that with three distinct utilities:
32
+
33
+ | | Solid's `deep` | `trackDeep` | `trackStore` | `captureStoreUpdates` |
34
+ | -------------------------------------- | -------------- | ----------- | ------------ | --------------------- |
35
+ | Tracks all nested changes | ✓ | ✓ | ✓ | ✓ |
36
+ | Returns live store proxy | — | ✓ | ✓ | — |
37
+ | Returns plain snapshot | ✓ | — | — | — |
38
+ | Works on plain objects wrapping stores | — | ✓ | — | — |
39
+ | Reports what changed and where | — | — | — | ✓ |
40
+
41
+ **Use Solid's `deep`** when you want to observe all changes and immediately consume a serializable value (e.g. persist to localStorage, send over the wire).
42
+
43
+ **Use `trackDeep` or `trackStore`** when you need the live reactive proxy back — for example, to pass it reactively to another primitive, or when you want to decide what to do with the store rather than serialize it immediately. `trackStore` is preferred for large or frequently updated stores due to its use of memoized structural subscriptions; `trackDeep` additionally accepts plain objects that contain stores.
44
+
45
+ **Use `captureStoreUpdates`** when you need to know _what_ changed and _where_ — it returns an array of `{ path, value }` deltas since the last call. Solid's `deep` has no equivalent for this.
46
+
17
47
  ## Installation
18
48
 
19
49
  ```bash
@@ -41,20 +71,23 @@ import { trackDeep } from "@solid-primitives/deep";
41
71
 
42
72
  const [state, setState] = createStore({ name: "John", age: 42 });
43
73
 
44
- createEffect(() => {
45
- trackDeep(state);
46
- /* execute some logic whenever the state changes */
47
- });
74
+ createEffect(
75
+ () => trackDeep(state),
76
+ () => {
77
+ /* execute some logic whenever the state changes */
78
+ },
79
+ );
48
80
  ```
49
81
 
50
82
  Or since this has a composable design, you can create _derivative_ functions and use them similar to derivative signals.
51
83
 
52
84
  ```ts
53
85
  const deeplyTrackedStore = () => trackDeep(sign);
54
- createEffect(() => {
55
- console.log("Store is: ", deeplyTrackedStore());
56
- // ^ this causes a re-execution of the effect on deep changes of properties
57
- });
86
+ createEffect(
87
+ () => deeplyTrackedStore(),
88
+ // ^ this causes a re-execution of the effect on deep changes of properties
89
+ value => console.log("Store is:", value),
90
+ );
58
91
  ```
59
92
 
60
93
  `trackDeep` will traverse any "wrappable" object _(objects that solid stores will wrap with proxies)_, even if it's not a solid store.
@@ -66,15 +99,17 @@ createEffect(() => {
66
99
  });
67
100
  ```
68
101
 
69
- > **Warning** If you `unwrap` a store, it will no longer be tracked by `trackDeep` nor `trackStore`!
102
+ > **Warning** If you `snapshot` a store, it will no longer be tracked by `trackDeep` nor `trackStore`!
70
103
 
71
104
  ```ts
72
- const unwrapped = unwrap(state);
105
+ import { snapshot } from "solid-js";
73
106
 
74
- createEffect(() => {
75
- // This will NOT work:
76
- trackDeep(unwrapped);
77
- });
107
+ const plain = snapshot(state);
108
+
109
+ createEffect(
110
+ () => trackDeep(plain), // This will NOT work — plain objects are not reactive
111
+ () => {},
112
+ );
78
113
  ```
79
114
 
80
115
  ## `trackStore`
@@ -92,10 +127,12 @@ import { trackStore } from "@solid-primitives/deep";
92
127
 
93
128
  const [state, setState] = createStore({ name: "John", age: 42 });
94
129
 
95
- createEffect(() => {
96
- trackStore(state);
97
- /* execute some logic whenever the state changes */
98
- });
130
+ createEffect(
131
+ () => trackStore(state),
132
+ () => {
133
+ /* execute some logic whenever the state changes */
134
+ },
135
+ );
99
136
  ```
100
137
 
101
138
  ## `captureStoreUpdates`
@@ -115,7 +152,9 @@ const getDelta = captureStoreUpdates(state);
115
152
 
116
153
  getDelta(); // [{ path: [], value: { todos: [] } }]
117
154
 
118
- setState("todos", ["foo"]);
155
+ setState(s => {
156
+ s.todos = ["foo"];
157
+ });
119
158
 
120
159
  getDelta(); // [{ path: ["todos"], value: ["foo"] }]
121
160
  ```
@@ -127,11 +166,13 @@ const [state, setState] = createStore({ todos: [] });
127
166
 
128
167
  const getDelta = captureStoreUpdates(state);
129
168
 
130
- createEffect(() => {
131
- const delta = getDelta();
132
- /* execute some logic whenever the state changes */
133
- console.log(delta);
134
- });
169
+ createEffect(
170
+ () => getDelta(),
171
+ delta => {
172
+ /* execute some logic whenever the state changes */
173
+ console.log(delta);
174
+ },
175
+ );
135
176
  ```
136
177
 
137
178
  The returned function is not a signal - it won't get updated by itself, it has to be called manually, or under a tracking scope to capture new updates.
@@ -144,18 +185,16 @@ const [state, setState] = createStore({ todos: [] });
144
185
  const delta = createMemo(captureStoreUpdates(state));
145
186
 
146
187
  // both of these effects will receive the same delta
147
- createEffect(() => {
148
- console.log(delta());
149
- });
150
- createEffect(() => {
151
- console.log(delta());
152
- });
188
+ createEffect(
189
+ () => delta(),
190
+ value => console.log(value),
191
+ );
192
+ createEffect(
193
+ () => delta(),
194
+ value => console.log(value),
195
+ );
153
196
  ```
154
197
 
155
- ### Demo
156
-
157
- See a demo of this primitive in action [here](https://primitives.solidjs.community/playground/deep).
158
-
159
198
  ## Changelog
160
199
 
161
200
  See [CHANGELOG.md](./CHANGELOG.md)
@@ -24,7 +24,7 @@ export type NestedUpdate<T> = {
24
24
  *
25
25
  * getDelta(); // [{ path: [], value: { todos: [] } }]
26
26
  *
27
- * setState("todos", ["foo"]);
27
+ * setState(s => { s.todos = ["foo"]; });
28
28
  *
29
29
  * getDelta(); // [{ path: ["todos"], value: ["foo"] }]
30
30
  * ```
@@ -1,7 +1,9 @@
1
1
  import { createLazyMemo } from "@solid-primitives/memo";
2
- import { $PROXY, $TRACK, createRoot, untrack } from "solid-js";
3
- import { unwrap } from "solid-js/store";
4
- import { isDev, isServer } from "solid-js/web";
2
+ import { $PROXY, $TRACK, DEV, runWithOwner, untrack, snapshot } from "solid-js";
3
+ import { isServer } from "@solidjs/web";
4
+ // Typed iterator that preserves numeric keys for arrays vs. string keys for objects.
5
+ // Object.entries() returns [string, T][] even for arrays, losing the numeric key type,
6
+ // so arrays are iterated manually.
5
7
  function* entries(obj) {
6
8
  if (Array.isArray(obj)) {
7
9
  for (let i = 0; i < obj.length; i++) {
@@ -14,18 +16,31 @@ function* entries(obj) {
14
16
  yield* Object.entries(obj)[Symbol.iterator]();
15
17
  }
16
18
  }
19
+ // One lazy memo per store node, keyed by node identity. The memo re-runs whenever the node's
20
+ // structure changes ([$TRACK] subscription) and returns the current set of child store nodes.
21
+ // Detached from any owner so it lives as long as the node is reachable, then self-disposes.
17
22
  const StoreNodeChildrenCache = new WeakMap();
23
+ // Returns the reactive snapshot of a node's direct store-node children.
24
+ // Uses a lazy memo so the computation is only created once per node, and only runs when read.
18
25
  function getStoreNodechildren(node) {
19
26
  let signal = StoreNodeChildrenCache.get(node);
20
27
  if (!signal) {
21
- const unwrapped = unwrap(node), isArray = Array.isArray(unwrapped);
22
- signal = createRoot(() => createLazyMemo(() => {
28
+ const isArray = Array.isArray(node);
29
+ // runWithOwner(null) detaches the memo from any current reactive owner so it won't be
30
+ // disposed when a caller's effect re-runs. It self-disposes when it has no subscribers.
31
+ signal = runWithOwner(null, () => createLazyMemo(() => {
32
+ // Subscribe to structural changes (key additions/removals) on this node.
23
33
  node[$TRACK];
34
+ // snapshot() inside untrack() gives us the plain key list without subscribing to
35
+ // individual property signals — we only want to know which keys exist, not their values.
36
+ const unwrapped = untrack(() => snapshot(node));
24
37
  const children = isArray ? [] : {};
25
38
  for (const [key, child] of entries(unwrapped)) {
26
39
  let childNode;
27
40
  if (child != null &&
28
41
  typeof child === "object" &&
42
+ // Prefer the proxy stored on the plain value ($PROXY), falling back to reading the
43
+ // key through the live store proxy (which re-wraps nested objects on access).
29
44
  ((childNode = child[$PROXY]) ||
30
45
  ((childNode = untrack(() => node[key])) && $TRACK in childNode))) {
31
46
  children[key] = childNode;
@@ -37,29 +52,42 @@ function getStoreNodechildren(node) {
37
52
  }
38
53
  return signal();
39
54
  }
55
+ // Module-level globals, reset at the start of every getDelta() call.
56
+ // Safe because JS is single-threaded — no two calls can interleave.
40
57
  let CurrentUpdates;
41
58
  let SeenNodes;
59
+ // Builds a fresh cache entry for a node that was added or changed.
60
+ // Recursively pre-populates children so future calls can diff them.
42
61
  function newCacheNode(children) {
43
62
  const record = { ...children };
44
63
  for (const [key, node] of entries(children)) {
45
64
  if (SeenNodes.has(node))
46
- continue;
65
+ continue; // guard against circular references
47
66
  SeenNodes.add(node);
48
67
  record[key] = newCacheNode(getStoreNodechildren(node));
49
68
  }
50
69
  return { children, record };
51
70
  }
71
+ // Walks the store tree, diffing each node against its cached snapshot.
72
+ // A node is "changed" when its children reference differs from the cached one —
73
+ // getStoreNodechildren() returns a stable reference when nothing has changed,
74
+ // so a strict equality check is sufficient and cheap.
75
+ // When a change is found, the whole subtree is re-cached and reported as a single
76
+ // update at the shallowest changed node (so leaf changes inside an object are reported
77
+ // as one update on the parent object, not one per leaf).
52
78
  function compareStoreWithCache(node, parent, key, path) {
53
79
  if (SeenNodes.has(node))
54
- return;
80
+ return; // guard against circular references
55
81
  SeenNodes.add(node);
56
82
  const cacheNode = parent[key], children = getStoreNodechildren(node);
57
83
  if (cacheNode && children === cacheNode.children) {
84
+ // Node itself is unchanged; recurse to check its children.
58
85
  for (const [key, child] of entries(children)) {
59
86
  compareStoreWithCache(child, cacheNode.record, key, [...path, key]);
60
87
  }
61
88
  }
62
89
  else {
90
+ // Node is new or its structure changed — record it and rebuild its subtree cache.
63
91
  parent[key] = newCacheNode(children);
64
92
  CurrentUpdates.push({ path, value: node });
65
93
  }
@@ -82,25 +110,30 @@ function compareStoreWithCache(node, parent, key, path) {
82
110
  *
83
111
  * getDelta(); // [{ path: [], value: { todos: [] } }]
84
112
  *
85
- * setState("todos", ["foo"]);
113
+ * setState(s => { s.todos = ["foo"]; });
86
114
  *
87
115
  * getDelta(); // [{ path: ["todos"], value: ["foo"] }]
88
116
  * ```
89
117
  */
90
118
  export function captureStoreUpdates(store) {
91
- // on the server you cannot check if the passed object is a store
92
- // so we just return the whole store always
93
- if (isServer || !($TRACK in store)) {
94
- if (isDev) {
119
+ // On the server $TRACK is not present on store proxies, so we can't diff.
120
+ // Return the whole store on the first call and nothing thereafter.
121
+ if (isServer) {
122
+ let init = true;
123
+ return () => (init ? ((init = false), [{ path: [], value: store }]) : []);
124
+ }
125
+ if (!(typeof store === "object" && store !== null && $TRACK in store)) {
126
+ if (DEV) {
95
127
  // eslint-disable-next-line no-console
96
- console.warn("createStoreDelta expects a store, got", store);
128
+ console.warn("captureStoreUpdates expects a store, got", store);
97
129
  }
98
130
  let init = true;
99
131
  return () => (init ? ((init = false), [{ path: [], value: store }]) : []);
100
132
  }
133
+ // The root cache entry — "root" is a synthetic key so compareStoreWithCache can write
134
+ // cache[key] uniformly without special-casing the top level.
101
135
  const cache = {};
102
136
  return () => {
103
- // set globals before each cycle
104
137
  CurrentUpdates = [];
105
138
  SeenNodes = new WeakSet();
106
139
  compareStoreWithCache(store, cache, "root", []);
@@ -1,4 +1,4 @@
1
- import { type Store } from "solid-js/store";
1
+ import { type Store } from "solid-js";
2
2
  /**
3
3
  * Tracks all properties of a {@link store} by iterating over them recursively.
4
4
  *
@@ -9,12 +9,12 @@ import { type Store } from "solid-js/store";
9
9
  *
10
10
  * @example
11
11
  * ```ts
12
- * createEffect(on(
12
+ * createEffect(
13
13
  * () => trackDeep(store),
14
14
  * () => {
15
15
  * // this effect will run when any property of store changes
16
16
  * }
17
- * ));
17
+ * );
18
18
  * ```
19
19
  */
20
20
  declare function trackDeep<T extends Store<object>>(store: T): T;
@@ -1,5 +1,4 @@
1
1
  import { $PROXY } from "solid-js";
2
- import {} from "solid-js/store";
3
2
  /**
4
3
  * Tracks all properties of a {@link store} by iterating over them recursively.
5
4
  *
@@ -10,12 +9,12 @@ import {} from "solid-js/store";
10
9
  *
11
10
  * @example
12
11
  * ```ts
13
- * createEffect(on(
12
+ * createEffect(
14
13
  * () => trackDeep(store),
15
14
  * () => {
16
15
  * // this effect will run when any property of store changes
17
16
  * }
18
- * ));
17
+ * );
19
18
  * ```
20
19
  */
21
20
  function trackDeep(store) {
@@ -25,7 +24,7 @@ function trackDeep(store) {
25
24
  function traverse(value, seen) {
26
25
  let isArray;
27
26
  let proto;
28
- // check the same conditions as in `isWrappable` from `/packages/solid/store/src/store.ts`
27
+ // check the same conditions as in `isWrappable` from solid's store implementation
29
28
  if (value != null &&
30
29
  typeof value === "object" &&
31
30
  !seen.has(value) &&
@@ -1,4 +1,4 @@
1
- import { type Store } from "solid-js/store";
1
+ import { type Store } from "solid-js";
2
2
  /**
3
3
  * Tracks all nested changes to passed {@link store}.
4
4
  *
@@ -9,12 +9,12 @@ import { type Store } from "solid-js/store";
9
9
  *
10
10
  * @example
11
11
  * ```ts
12
- * createEffect(on(
12
+ * createEffect(
13
13
  * () => trackStore(store),
14
14
  * () => {
15
15
  * // this effect will run when any property of store changes
16
16
  * }
17
- * ));
17
+ * );
18
18
  * ```
19
19
  */
20
20
  declare function trackStore<T extends object>(store: Store<T>): T;
@@ -1,54 +1,4 @@
1
- import { $PROXY, $TRACK, createMemo, createRoot, createSignal, untrack } from "solid-js";
2
- import { unwrap } from "solid-js/store";
3
- const EQUALS_FALSE = { equals: false };
4
- const TrackStoreCache = new WeakMap();
5
- // for preventing the same object to be visited multiple times in the same trackStore call
6
- let TrackVersion = 0;
7
- function getTrackStoreNode(node) {
8
- let track = TrackStoreCache.get(node);
9
- if (!track) {
10
- createRoot(() => {
11
- const unwrapped = unwrap(node);
12
- // custom lazy memo to support circular references
13
- // maybe it won't be needed in solid 2.0
14
- let is_reading = false;
15
- let is_stale = true;
16
- let version = 0;
17
- const [signal, trigger] = createSignal(undefined, EQUALS_FALSE);
18
- const memo = createMemo(() => {
19
- if (is_reading) {
20
- node[$TRACK]; // shallow track store node
21
- // track each child node
22
- for (const [key, child] of Object.entries(unwrapped)) {
23
- let childNode;
24
- if (child != null &&
25
- typeof child === "object" &&
26
- ((childNode = child[$PROXY]) || $TRACK in (childNode = untrack(() => node[key])))) {
27
- getTrackStoreNode(childNode)?.();
28
- }
29
- }
30
- }
31
- else {
32
- signal();
33
- is_stale = true;
34
- }
35
- }, undefined, EQUALS_FALSE);
36
- track = () => {
37
- is_reading = true;
38
- if (is_stale) {
39
- trigger();
40
- is_stale = false;
41
- }
42
- const already_tracked = version === TrackVersion;
43
- version = TrackVersion;
44
- already_tracked || memo();
45
- is_reading = false;
46
- };
47
- TrackStoreCache.set(node, track);
48
- });
49
- }
50
- return track;
51
- }
1
+ import { $TRACK } from "solid-js";
52
2
  /**
53
3
  * Tracks all nested changes to passed {@link store}.
54
4
  *
@@ -59,17 +9,29 @@ function getTrackStoreNode(node) {
59
9
  *
60
10
  * @example
61
11
  * ```ts
62
- * createEffect(on(
12
+ * createEffect(
63
13
  * () => trackStore(store),
64
14
  * () => {
65
15
  * // this effect will run when any property of store changes
66
16
  * }
67
- * ));
17
+ * );
68
18
  * ```
69
19
  */
70
20
  function trackStore(store) {
71
- TrackVersion++;
72
- $TRACK in store && getTrackStoreNode(store)?.();
21
+ traverseStore(store, new Set());
73
22
  return store;
74
23
  }
24
+ function traverseStore(node, seen) {
25
+ if (!($TRACK in node) || seen.has(node))
26
+ return;
27
+ seen.add(node);
28
+ // subscribe to structural changes (additions/removals)
29
+ node[$TRACK];
30
+ // access all values through the proxy to subscribe to getters and collect children
31
+ for (const child of Object.values(node)) {
32
+ if (child != null && typeof child === "object") {
33
+ traverseStore(child, seen);
34
+ }
35
+ }
36
+ }
75
37
  export { trackStore };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solid-primitives/deep",
3
- "version": "0.3.6",
3
+ "version": "1.0.0-next.0",
4
4
  "description": "Primitives for tracking and observing nested reactive objects in Solid.",
5
5
  "author": "Samuel Burbano <me@iosamuel.dev>",
6
6
  "contributors": [
@@ -17,13 +17,14 @@
17
17
  },
18
18
  "primitive": {
19
19
  "name": "deep",
20
- "stage": 1,
20
+ "stage": 3,
21
21
  "list": [
22
22
  "trackDeep",
23
23
  "trackStore",
24
24
  "captureStoreUpdates"
25
25
  ],
26
- "category": "Reactivity"
26
+ "category": "Reactivity",
27
+ "gzip": 1298
27
28
  },
28
29
  "keywords": [
29
30
  "solid",
@@ -51,16 +52,17 @@
51
52
  },
52
53
  "typesVersions": {},
53
54
  "dependencies": {
54
- "@solid-primitives/memo": "^1.5.0"
55
+ "@solid-primitives/memo": "^2.0.0-next.0"
55
56
  },
56
57
  "peerDependencies": {
57
- "solid-js": "^1.6.12"
58
+ "@solidjs/web": "^2.0.0-beta.15",
59
+ "solid-js": "^2.0.0-beta.15"
58
60
  },
59
61
  "devDependencies": {
60
- "solid-js": "^1.9.7"
62
+ "@solidjs/web": "2.0.0-beta.15",
63
+ "solid-js": "2.0.0-beta.15"
61
64
  },
62
65
  "scripts": {
63
- "dev": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/dev.ts",
64
66
  "build": "node --import=@nothing-but/node-resolve-ts --experimental-transform-types ../../scripts/build.ts",
65
67
  "vitest": "vitest -c ../../configs/vitest.config.ts",
66
68
  "test": "pnpm run vitest",