diff --git a/.changeset/deep-solid2-migration.md b/.changeset/deep-solid2-migration.md new file mode 100644 index 000000000..a4b5f5126 --- /dev/null +++ b/.changeset/deep-solid2-migration.md @@ -0,0 +1,18 @@ +--- +"@solid-primitives/deep": major +--- + +Migrate to Solid.js v2.0 (beta.13) + +## Breaking Changes + +**Peer dependencies**: `solid-js@^2.0.0-beta.13` and `@solidjs/web@^2.0.0-beta.13` are now required. + +### `@solid-primitives/deep` + +- `isServer` now imported from `@solidjs/web` (not `solid-js/web`) +- Store imports (`createStore`, `reconcile`, `snapshot`) moved from `solid-js/store` to `solid-js` +- `unwrap` replaced by `snapshot` — returns a plain non-reactive copy of a store +- Store setters now use draft-first form: `setState(s => { s.prop = value; })` replaces path-based setters +- `createEffect` in examples updated to required split compute/apply form +- `trackStore` now correctly handles getters (subscribes to reactive deps accessed through getters) and rejects plain object wrappers around stores (`pojo: false` behavior preserved) diff --git a/packages/deep/README.md b/packages/deep/README.md index 58a865d0b..14c7d0915 100644 --- a/packages/deep/README.md +++ b/packages/deep/README.md @@ -41,20 +41,23 @@ import { trackDeep } from "@solid-primitives/deep"; const [state, setState] = createStore({ name: "John", age: 42 }); -createEffect(() => { - trackDeep(state); - /* execute some logic whenever the state changes */ -}); +createEffect( + () => trackDeep(state), + () => { + /* execute some logic whenever the state changes */ + } +); ``` Or since this has a composable design, you can create _derivative_ functions and use them similar to derivative signals. ```ts const deeplyTrackedStore = () => trackDeep(sign); -createEffect(() => { - console.log("Store is: ", deeplyTrackedStore()); - // ^ this causes a re-execution of the effect on deep changes of properties -}); +createEffect( + () => deeplyTrackedStore(), + // ^ this causes a re-execution of the effect on deep changes of properties + value => console.log("Store is:", value) +); ``` `trackDeep` will traverse any "wrappable" object _(objects that solid stores will wrap with proxies)_, even if it's not a solid store. @@ -66,15 +69,17 @@ createEffect(() => { }); ``` -> **Warning** If you `unwrap` a store, it will no longer be tracked by `trackDeep` nor `trackStore`! +> **Warning** If you `snapshot` a store, it will no longer be tracked by `trackDeep` nor `trackStore`! ```ts -const unwrapped = unwrap(state); +import { snapshot } from "solid-js"; -createEffect(() => { - // This will NOT work: - trackDeep(unwrapped); -}); +const plain = snapshot(state); + +createEffect( + () => trackDeep(plain), // This will NOT work — plain objects are not reactive + () => {} +); ``` ## `trackStore` @@ -92,10 +97,12 @@ import { trackStore } from "@solid-primitives/deep"; const [state, setState] = createStore({ name: "John", age: 42 }); -createEffect(() => { - trackStore(state); - /* execute some logic whenever the state changes */ -}); +createEffect( + () => trackStore(state), + () => { + /* execute some logic whenever the state changes */ + } +); ``` ## `captureStoreUpdates` @@ -115,7 +122,7 @@ const getDelta = captureStoreUpdates(state); getDelta(); // [{ path: [], value: { todos: [] } }] -setState("todos", ["foo"]); +setState(s => { s.todos = ["foo"]; }); getDelta(); // [{ path: ["todos"], value: ["foo"] }] ``` @@ -127,11 +134,13 @@ const [state, setState] = createStore({ todos: [] }); const getDelta = captureStoreUpdates(state); -createEffect(() => { - const delta = getDelta(); - /* execute some logic whenever the state changes */ - console.log(delta); -}); +createEffect( + () => getDelta(), + delta => { + /* execute some logic whenever the state changes */ + console.log(delta); + } +); ``` 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,12 +153,14 @@ const [state, setState] = createStore({ todos: [] }); const delta = createMemo(captureStoreUpdates(state)); // both of these effects will receive the same delta -createEffect(() => { - console.log(delta()); -}); -createEffect(() => { - console.log(delta()); -}); +createEffect( + () => delta(), + value => console.log(value) +); +createEffect( + () => delta(), + value => console.log(value) +); ``` ### Demo diff --git a/packages/deep/package.json b/packages/deep/package.json index 0bb6b97db..4af5d3c07 100644 --- a/packages/deep/package.json +++ b/packages/deep/package.json @@ -61,9 +61,11 @@ "@solid-primitives/memo": "workspace:^" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.13", + "solid-js": "^2.0.0-beta.13" }, "devDependencies": { - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.13", + "solid-js": "2.0.0-beta.13" } } diff --git a/packages/deep/src/store-updates.ts b/packages/deep/src/store-updates.ts index ec2879d0d..d78f63a77 100644 --- a/packages/deep/src/store-updates.ts +++ b/packages/deep/src/store-updates.ts @@ -1,10 +1,13 @@ import { createLazyMemo } from "@solid-primitives/memo"; -import { $PROXY, $TRACK, type Accessor, createRoot, untrack } from "solid-js"; -import { unwrap } from "solid-js/store"; -import { isDev, isServer } from "solid-js/web"; +import { $PROXY, $TRACK, type Accessor, DEV, runWithOwner, untrack, snapshot } from "solid-js"; +import { isServer } from "@solidjs/web"; +// Indexable by string or number keys — matches what Solid stores can wrap. type Static = { [K in number | string]: T } | T[]; +// Typed iterator that preserves numeric keys for arrays vs. string keys for objects. +// Object.entries() returns [string, T][] even for arrays, losing the numeric key type, +// so arrays are iterated manually. function* entries( obj: T, ): IterableIterator { @@ -19,27 +22,41 @@ function* entries( } } +// A store node is any object that Solid has wrapped with a reactive proxy ($TRACK marks it). type StoreNode = Record & Static; +// The set of direct store-node children of a given node (non-store leaf values are excluded). type StoreNodeChildren = Static; +// One lazy memo per store node, keyed by node identity. The memo re-runs whenever the node's +// structure changes ([$TRACK] subscription) and returns the current set of child store nodes. +// Detached from any owner so it lives as long as the node is reachable, then self-disposes. const StoreNodeChildrenCache = new WeakMap>(); +// Returns the reactive snapshot of a node's direct store-node children. +// Uses a lazy memo so the computation is only created once per node, and only runs when read. function getStoreNodechildren(node: StoreNode): StoreNodeChildren { let signal = StoreNodeChildrenCache.get(node); if (!signal) { - const unwrapped = unwrap(node), - isArray = Array.isArray(unwrapped); + const isArray = Array.isArray(node); - signal = createRoot(() => + // runWithOwner(null) detaches the memo from any current reactive owner so it won't be + // disposed when a caller's effect re-runs. It self-disposes when it has no subscribers. + signal = runWithOwner(null, () => createLazyMemo(() => { + // Subscribe to structural changes (key additions/removals) on this node. node[$TRACK]; + // snapshot() inside untrack() gives us the plain key list without subscribing to + // individual property signals — we only want to know which keys exist, not their values. + const unwrapped = untrack(() => snapshot(node)); const children: StoreNodeChildren = isArray ? [] : {}; for (const [key, child] of entries(unwrapped)) { let childNode: any; if ( child != null && typeof child === "object" && + // Prefer the proxy stored on the plain value ($PROXY), falling back to reading the + // key through the live store proxy (which re-wraps nested objects on access). ((childNode = (child as any)[$PROXY]) || ((childNode = untrack(() => node[key as any])) && $TRACK in childNode)) ) { @@ -63,6 +80,8 @@ export type NestedUpdate = { value: AllNestedObjects; }; +// Per-node cache entry: the children snapshot from the last getDelta() call, plus a +// recursively-shaped record mirroring the same structure so we can walk the tree. type StoreUpdateCache = { [K in string | number]: { children: StoreNodeChildren; @@ -70,14 +89,18 @@ type StoreUpdateCache = { }; }; +// Module-level globals, reset at the start of every getDelta() call. +// Safe because JS is single-threaded — no two calls can interleave. let CurrentUpdates!: NestedUpdate[]; let SeenNodes!: WeakSet; +// Builds a fresh cache entry for a node that was added or changed. +// Recursively pre-populates children so future calls can diff them. function newCacheNode(children: StoreNodeChildren): StoreUpdateCache[number] { const record: StoreUpdateCache = { ...children } as any; for (const [key, node] of entries(children)) { - if (SeenNodes.has(node)) continue; + if (SeenNodes.has(node)) continue; // guard against circular references SeenNodes.add(node); record[key] = newCacheNode(getStoreNodechildren(node)); } @@ -85,23 +108,32 @@ function newCacheNode(children: StoreNodeChildren): StoreUpdateCache[number] { return { children, record }; } +// Walks the store tree, diffing each node against its cached snapshot. +// A node is "changed" when its children reference differs from the cached one — +// getStoreNodechildren() returns a stable reference when nothing has changed, +// so a strict equality check is sufficient and cheap. +// When a change is found, the whole subtree is re-cached and reported as a single +// update at the shallowest changed node (so leaf changes inside an object are reported +// as one update on the parent object, not one per leaf). function compareStoreWithCache( node: StoreNode, parent: StoreUpdateCache, key: string | number, path: (string | number)[], ): void { - if (SeenNodes.has(node)) return; + if (SeenNodes.has(node)) return; // guard against circular references SeenNodes.add(node); const cacheNode = parent[key], children = getStoreNodechildren(node); if (cacheNode && children === cacheNode.children) { + // Node itself is unchanged; recurse to check its children. for (const [key, child] of entries(children)) { compareStoreWithCache(child, cacheNode.record, key, [...path, key]); } } else { + // Node is new or its structure changed — record it and rebuild its subtree cache. parent[key] = newCacheNode(children); CurrentUpdates.push({ path, value: node }); } @@ -125,28 +157,33 @@ function compareStoreWithCache( * * getDelta(); // [{ path: [], value: { todos: [] } }] * - * setState("todos", ["foo"]); + * setState(s => { s.todos = ["foo"]; }); * * getDelta(); // [{ path: ["todos"], value: ["foo"] }] * ``` */ export function captureStoreUpdates(store: T): () => NestedUpdate[] { - // on the server you cannot check if the passed object is a store - // so we just return the whole store always - if (isServer || !($TRACK in store)) { - if (isDev) { + // On the server $TRACK is not present on store proxies, so we can't diff. + // Return the whole store on the first call and nothing thereafter. + if (isServer) { + let init = true; + return () => (init ? ((init = false), [{ path: [], value: store as any }]) : []); + } + + if (!(typeof store === "object" && store !== null && $TRACK in store)) { + if (DEV) { // eslint-disable-next-line no-console - console.warn("createStoreDelta expects a store, got", store); + console.warn("captureStoreUpdates expects a store, got", store); } - let init = true; return () => (init ? ((init = false), [{ path: [], value: store as any }]) : []); } + // The root cache entry — "root" is a synthetic key so compareStoreWithCache can write + // cache[key] uniformly without special-casing the top level. const cache: StoreUpdateCache = {}; return () => { - // set globals before each cycle CurrentUpdates = []; SeenNodes = new WeakSet(); diff --git a/packages/deep/src/track-deep.ts b/packages/deep/src/track-deep.ts index 4019c3e06..c03bfe325 100644 --- a/packages/deep/src/track-deep.ts +++ b/packages/deep/src/track-deep.ts @@ -1,5 +1,4 @@ -import { $PROXY } from "solid-js"; -import { type Store } from "solid-js/store"; +import { $PROXY, type Store } from "solid-js"; /** * Tracks all properties of a {@link store} by iterating over them recursively. @@ -11,12 +10,12 @@ import { type Store } from "solid-js/store"; * * @example * ```ts - * createEffect(on( + * createEffect( * () => trackDeep(store), * () => { * // this effect will run when any property of store changes * } - * )); + * ); * ``` */ function trackDeep>(store: T): T { @@ -24,10 +23,10 @@ function trackDeep>(store: T): T { return store; } -function traverse(value: Store, seen: Set): void { +function traverse(value: unknown, seen: Set): void { let isArray: boolean; let proto; - // check the same conditions as in `isWrappable` from `/packages/solid/store/src/store.ts` + // check the same conditions as in `isWrappable` from solid's store implementation if ( value != null && typeof value === "object" && diff --git a/packages/deep/src/track-store.ts b/packages/deep/src/track-store.ts index e6adcb8ff..0a77ab6b1 100644 --- a/packages/deep/src/track-store.ts +++ b/packages/deep/src/track-store.ts @@ -1,74 +1,4 @@ -import { $PROXY, $TRACK, createMemo, createRoot, createSignal, untrack } from "solid-js"; -import { type Store, unwrap } from "solid-js/store"; - -const EQUALS_FALSE = { equals: false } as const; - -type StoreNode = Record; - -const TrackStoreCache = new WeakMap(); - -// for preventing the same object to be visited multiple times in the same trackStore call -let TrackVersion = 0; - -function getTrackStoreNode(node: StoreNode): VoidFunction | undefined { - let track = TrackStoreCache.get(node); - - if (!track) { - createRoot(() => { - const unwrapped = unwrap(node); - - // custom lazy memo to support circular references - // maybe it won't be needed in solid 2.0 - - let is_reading = false; - let is_stale = true; - let version = 0; - - const [signal, trigger] = createSignal(undefined, EQUALS_FALSE); - - const memo = createMemo( - () => { - if (is_reading) { - node[$TRACK]; // shallow track store node - - // track each child node - for (const [key, child] of Object.entries(unwrapped)) { - let childNode: StoreNode; - if ( - child != null && - typeof child === "object" && - ((childNode = child[$PROXY]) || $TRACK in (childNode = untrack(() => node[key]))) - ) { - getTrackStoreNode(childNode)?.(); - } - } - } else { - signal(); - is_stale = true; - } - }, - undefined, - EQUALS_FALSE, - ); - - track = () => { - is_reading = true; - if (is_stale) { - trigger(); - is_stale = false; - } - const already_tracked = version === TrackVersion; - version = TrackVersion; - already_tracked || memo(); - is_reading = false; - }; - - TrackStoreCache.set(node, track); - }); - } - - return track; -} +import { $TRACK, type Store } from "solid-js"; /** * Tracks all nested changes to passed {@link store}. @@ -80,18 +10,30 @@ function getTrackStoreNode(node: StoreNode): VoidFunction | undefined { * * @example * ```ts - * createEffect(on( + * createEffect( * () => trackStore(store), * () => { * // this effect will run when any property of store changes * } - * )); + * ); * ``` */ function trackStore(store: Store): T { - TrackVersion++; - $TRACK in store && getTrackStoreNode(store)?.(); + traverseStore(store, new Set()); return store; } +function traverseStore(node: any, seen: Set): void { + if (!($TRACK in node) || seen.has(node)) return; + seen.add(node); + // subscribe to structural changes (additions/removals) + node[$TRACK]; + // access all values through the proxy to subscribe to getters and collect children + for (const child of Object.values(node)) { + if (child != null && typeof child === "object") { + traverseStore(child, seen); + } + } +} + export { trackStore }; diff --git a/packages/deep/test/track.test.ts b/packages/deep/test/track.test.ts index 52ffa5f2c..a3f8ca39b 100644 --- a/packages/deep/test/track.test.ts +++ b/packages/deep/test/track.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; -import { batch, createEffect, createRoot, createSignal } from "solid-js"; +import { createEffect, createRoot, createSignal, flush } from "solid-js"; +import { createStore, snapshot } from "solid-js"; import { captureStoreUpdates, trackDeep, trackStore } from "../src/index.js"; -import { createStore, reconcile, unwrap } from "solid-js/store"; const apis: { name: string; @@ -34,18 +34,21 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("a", "a.b", "minds"); + set(s => { s.a["a.b"] = "minds"; }); + flush(); expect(runs).toBe(2); - set("b", "bar"); + set(s => { s.b = "bar"; }); + flush(); expect(runs).toBe(3); }); @@ -55,22 +58,25 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(2); - set("a", "a.b", "minds"); + set(s => { s.a["a.b"] = "minds"; }); + flush(); expect(runs).toBe(4); - set("b", "bar"); + set(s => { s.b = "bar"; }); + flush(); expect(runs).toBe(6); }); @@ -80,17 +86,17 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - batch(() => { - set("a", "a.b", "minds"); - set("b", "bar"); - }); + set(s => { s.a["a.b"] = "minds"; }); + set(s => { s.b = "bar"; }); + flush(); expect(runs).toBe(2); }); @@ -100,14 +106,16 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("b", "foo"); + set((s: any) => { s.b = "foo"; }); + flush(); expect(runs).toBe(2); }); @@ -117,14 +125,16 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("b", undefined); + set(s => { (s as any).b = undefined; }); + flush(); expect(runs).toBe(2); }); @@ -134,17 +144,20 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set({ a: { "a.b": "minds" } }); + set(() => ({ a: { "a.b": "minds" } })); + flush(); expect(runs).toBe(2); - set("a", "a.b", "thoughts"); + set(s => { s.a["a.b"] = "thoughts"; }); + flush(); expect(runs).toBe(3); }); @@ -154,14 +167,16 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("a", [2, 3, 1]); + set(s => { s.a[0] = 2; s.a[1] = 3; s.a[2] = 1; }); + flush(); expect(runs).toBe(2); }); @@ -175,14 +190,16 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs += 1; - }); + createEffect( + () => fn(), + () => { runs += 1; }, + ); }); + flush(); expect(runs).toBe(1); - set("count", 1); + set(s => { s.count = 1; }); + flush(); expect(runs).toBe(2); }); @@ -199,20 +216,22 @@ for (const api of apis) { let runs_leaf = 0; createRoot(() => { - createEffect(() => { - fn_root(); - runs_root += 1; - }); - createEffect(() => { - fn_leaf(); - runs_leaf += 1; - }); + createEffect( + () => fn_root(), + () => { runs_root += 1; }, + ); + createEffect( + () => fn_leaf(), + () => { runs_leaf += 1; }, + ); }); + flush(); expect(runs_root).toBe(1); expect(runs_leaf).toBe(1); - set("count", 1); + set(s => { s.count = 1; }); + flush(); expect(runs_root).toBe(2); expect(runs_leaf).toBe(2); }); @@ -225,18 +244,21 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("a", "count", 1); + set(s => { s.a.count = 1; }); + flush(); expect(runs).toBe(2); - set("b", "count", 2); + set(s => { s.b.count = 2; }); + flush(); expect(runs).toBe(3); }); @@ -251,24 +273,27 @@ for (const api of apis) { let runs_b = 0; createRoot(() => { - createEffect(() => { - fn_a(); - runs_a++; - }); - createEffect(() => { - fn_b(); - runs_b++; - }); + createEffect( + () => fn_a(), + () => { runs_a++; }, + ); + createEffect( + () => fn_b(), + () => { runs_b++; }, + ); }); + flush(); expect(runs_a).toBe(1); expect(runs_b).toBe(1); - set("a", "count", 1); + set(s => { s.a.count = 1; }); + flush(); expect(runs_a).toBe(2); expect(runs_b).toBe(2); - set("b", "count", 2); + set(s => { s.b.count = 2; }); + flush(); expect(runs_a).toBe(3); expect(runs_b).toBe(3); }); @@ -279,16 +304,21 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => fn); + createEffect( + () => fn(), + () => {}, + ); const a = sign.a; - createEffect(() => { - api.fn(a); - runs++; - }); + createEffect( + () => { api.fn(a); }, + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("b", "foo"); + set((s: any) => { s.b = "foo"; }); + flush(); expect(runs).toBe(1); }); @@ -298,35 +328,40 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("a", reconcile({ foo: "bar" })); + set((s: any) => { s.a = { foo: "bar" }; }); + flush(); expect(runs).toBe(2); - set("a", "foo", "baz"); + set((s: any) => { s.a.foo = "baz"; }); + flush(); expect(runs).toBe(3); }); test("unwrapped", () => { const [sign, set] = createStore({ a: { "a.b": "thoughts" } }); - const unwrapped = unwrap(sign); + const unwrapped = snapshot(sign); const fn = api.fn(unwrapped); let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("a", "a.b", "minds"); + set(s => { s.a["a.b"] = "minds"; }); + flush(); expect(runs).toBe(1); }); @@ -336,14 +371,16 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); - set("a", "a.b", "minds"); + set(s => { s.a["a.b"] = "minds"; }); + flush(); if (api.pojo) { expect(runs).toBe(2); } else { @@ -363,14 +400,16 @@ for (const api of apis) { let runs = 0; createRoot(() => { - createEffect(() => { - fn(); - runs++; - }); + createEffect( + () => fn(), + () => { runs++; }, + ); }); + flush(); expect(runs).toBe(1); setCount(1); + flush(); expect(runs).toBe(2); }); }); diff --git a/packages/deep/test/updates.test.ts b/packages/deep/test/updates.test.ts index 100966c6f..c1b1533f4 100644 --- a/packages/deep/test/updates.test.ts +++ b/packages/deep/test/updates.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "vitest"; -import { batch, createEffect, createRoot, createSignal } from "solid-js"; +import { createEffect, createRoot, createSignal, flush } from "solid-js"; +import { createStore, reconcile } from "solid-js"; import { captureStoreUpdates } from "../src/index.js"; -import { createStore } from "solid-js/store"; describe("createStoreDelta", () => { test("initial value", () => { @@ -22,12 +22,15 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - set("a", "a.b", "minds"); + set(s => { s.a["a.b"] = "minds"; }); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ { @@ -36,7 +39,8 @@ describe("createStoreDelta", () => { }, ] satisfies ReturnType); - set("b", "bar"); + set(s => { s.b = "bar"; }); + flush(); expect(captured).toHaveLength(3); expect(captured[2]).toEqual([ { @@ -52,15 +56,16 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - batch(() => { - set("a", "ab", "minds"); - set("b", "ba", 2); - }); + set(s => { s.a.ab = "minds"; }); + set(s => { s.b.ba = 2; }); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ @@ -81,15 +86,16 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - batch(() => { - set("a", "a.b", "minds"); - set("b", "bar"); - }); + set(s => { s.a["a.b"] = "minds"; }); + set(s => { s.b = "bar"; }); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ @@ -104,16 +110,18 @@ describe("createStoreDelta", () => { const captured: any[] = []; const [sign] = createStore({ a: { "a.b": "thoughts" } }); const diff = captureStoreUpdates(sign); - const [track, trigger] = createSignal(undefined, { equals: false }); + const [track, trigger] = createSignal(undefined, { equals: false }); createRoot(() => { - createEffect(() => { - track(); - captured.push(diff()); - }); + createEffect( + () => { track(); return diff(); }, + updates => { captured.push(updates); }, + ); }); + flush(); - trigger(); + trigger(undefined); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([]); }); @@ -124,12 +132,15 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - set("b", "foo"); + set((s: any) => { s.b = "foo"; }); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ { @@ -145,12 +156,15 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - set("b", undefined); + set(s => { (s as any).b = undefined; }); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ { @@ -166,12 +180,15 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - set({ a: { "a.b": "minds" } }); + set(() => ({ a: { "a.b": "minds" } })); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ { @@ -187,12 +204,15 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - set("b", 0, "bar"); + set(s => { s.b[0] = "bar"; }); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ { @@ -201,7 +221,8 @@ describe("createStoreDelta", () => { }, ] satisfies ReturnType); - set("b", 1, "baz"); + set(s => { (s.b as any)[1] = "baz"; }); + flush(); expect(captured).toHaveLength(3); expect(captured[2]).toEqual([ { @@ -217,12 +238,15 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - set(0, "n", 1); + set(s => { s[0].n = 1; }); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ { @@ -238,12 +262,15 @@ describe("createStoreDelta", () => { const diff = captureStoreUpdates(sign); createRoot(() => { - createEffect(() => { - captured.push(diff()); - }); + createEffect( + () => diff(), + updates => { captured.push(updates); }, + ); }); + flush(); - set("a", "ab", sign); + set((s: any) => { s.a.ab = sign; }); + flush(); expect(captured).toHaveLength(2); expect(captured[1]).toEqual([ { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee1923c5f..9b7165ff2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -278,9 +278,12 @@ importers: specifier: workspace:^ version: link:../memo devDependencies: + '@solidjs/web': + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13(solid-js@2.0.0-beta.13) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.13 + version: 2.0.0-beta.13 packages/destructure: dependencies: @@ -2719,6 +2722,9 @@ packages: peerDependencies: solid-js: '>=1.8.4' + '@solidjs/signals@2.0.0-beta.13': + resolution: {integrity: sha512-jc+wLRK+eyUFerH8Mjed4HikdJwz3z95TT7tqyG+K00IV9jfgZWvR1nZDUEZc6kXffmtW/z+w6PNn62ea5KjIw==} + '@solidjs/signals@2.0.0-beta.14': resolution: {integrity: sha512-y72nYtD7ogwX/UR5g2Y+meyeO6Q/xbQGtmvVTQX6USkMwEGOMnytqDnHj5amUzD7Fzqg32svwtCSx/q8hsOXAA==} @@ -5262,12 +5268,6 @@ packages: peerDependencies: seroval: ^1.0 - seroval-plugins@1.5.2: - resolution: {integrity: sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - seroval-plugins@1.5.4: resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} @@ -5278,10 +5278,6 @@ packages: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} - seroval@1.5.2: - resolution: {integrity: sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q==} - engines: {node: '>=10'} - seroval@1.5.4: resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} engines: {node: '>=10'} @@ -5341,6 +5337,9 @@ packages: solid-js@1.9.7: resolution: {integrity: sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==} + solid-js@2.0.0-beta.13: + resolution: {integrity: sha512-uAknr7Xkn25zAufBrYko4eOCbcg/gkrwnmE9KVb2Kb3vVZw2ibqseNxpjslnwJkT4gFScmFniqJtzRp7vO2klA==} + solid-js@2.0.0-beta.14: resolution: {integrity: sha512-gbbvlxhs1GgL1IsnwHNtkTCRBBQcIDMwznBw3T05iYvP+fuUKMyIPku+ZLjeALyX4RaSLR99JSL6NttyHsYb8Q==} @@ -7734,12 +7733,20 @@ snapshots: dependencies: solid-js: 2.0.0-beta.14 + '@solidjs/signals@2.0.0-beta.13': {} + '@solidjs/signals@2.0.0-beta.14': {} + '@solidjs/web@2.0.0-beta.13(solid-js@2.0.0-beta.13)': + dependencies: + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + solid-js: 2.0.0-beta.13 + '@solidjs/web@2.0.0-beta.13(solid-js@2.0.0-beta.14)': dependencies: - seroval: 1.5.2 - seroval-plugins: 1.5.2(seroval@1.5.2) + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) solid-js: 2.0.0-beta.14 '@solidjs/web@2.0.0-beta.14(solid-js@2.0.0-beta.14)': @@ -10704,18 +10711,12 @@ snapshots: dependencies: seroval: 1.3.2 - seroval-plugins@1.5.2(seroval@1.5.2): - dependencies: - seroval: 1.5.2 - seroval-plugins@1.5.4(seroval@1.5.4): dependencies: seroval: 1.5.4 seroval@1.3.2: {} - seroval@1.5.2: {} - seroval@1.5.4: {} set-blocking@2.0.0: {} @@ -10771,6 +10772,13 @@ snapshots: seroval: 1.3.2 seroval-plugins: 1.3.2(seroval@1.3.2) + solid-js@2.0.0-beta.13: + dependencies: + '@solidjs/signals': 2.0.0-beta.13 + csstype: 3.1.3 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) + solid-js@2.0.0-beta.14: dependencies: '@solidjs/signals': 2.0.0-beta.14