diff --git a/packages/destructure/README.md b/packages/destructure/README.md index b52e8604e..8ffeaef31 100644 --- a/packages/destructure/README.md +++ b/packages/destructure/README.md @@ -15,9 +15,13 @@ Primitive for destructuring reactive objects _– like props or stores –_ or s ```bash npm install @solid-primitives/destructure # or +pnpm add @solid-primitives/destructure +# or yarn add @solid-primitives/destructure ``` +> Requires **Solid 2.0** (`solid-js ^2.0.0-beta.10`). For Solid 1.x use v0.2.x. + ## `destructure` Spreads an reactive object _(store or props)_ or a reactive object signal into a tuple/map of signals for each object key. @@ -30,11 +34,13 @@ import { destructure } from "@solid-primitives/destructure"; #### How to use it -`destructure` is an reactive primitive, hence needs to be used under an reactive root. Pass an reactive object or a signal as it's first argument, and configure it's behavior via options: +`destructure` is a reactive primitive, hence needs to be used under a reactive root. Pass a reactive object or a signal as its first argument, and configure its behavior via options: - `memo` - wraps accessors in `createMemo`, making each property update independently. _(enabled by default for signal source)_ -- `lazy` - property accessors are created on key read. enable if you want to only a subset of source properties, or use properties initially missing -- `deep` - destructure nested objects +- `lazy` - property accessors are created on key read. enable if you want only a subset of source properties, or to use properties initially missing +- `deep` - destructure nested objects recursively +- `name` - debug name passed to internal `createMemo` calls _(dev mode only)_ +- `equals` - custom equality function passed to internal `createMemo` calls ```ts // spread tuples diff --git a/packages/destructure/package.json b/packages/destructure/package.json index f78ba306e..fa3ad928a 100644 --- a/packages/destructure/package.json +++ b/packages/destructure/package.json @@ -1,6 +1,6 @@ { "name": "@solid-primitives/destructure", - "version": "0.2.3", + "version": "0.3.0", "description": "Primitives for destructuring reactive objects – like props or stores – or signals of them into a separate accessors updated individually.", "author": "Damian Tarnawski @thetarnav ", "license": "MIT", @@ -53,10 +53,10 @@ "@solid-primitives/utils": "workspace:^" }, "peerDependencies": { - "solid-js": "^1.6.12" + "solid-js": "^2.0.0-beta.10" }, "typesVersions": {}, "devDependencies": { - "solid-js": "^1.9.7" + "solid-js": "2.0.0-beta.10" } } diff --git a/packages/destructure/src/index.ts b/packages/destructure/src/index.ts index e637a0e5a..12eb45a6b 100644 --- a/packages/destructure/src/index.ts +++ b/packages/destructure/src/index.ts @@ -1,4 +1,4 @@ -import { createMemo, type Accessor, runWithOwner, getOwner, type MemoOptions } from "solid-js"; +import { createMemo, type Accessor, type MemoOptions, runWithOwner, getOwner } from "solid-js"; import { access, type MaybeAccessor, @@ -102,7 +102,7 @@ export function destructure destructure(calc, { ...config, memo })); - return memo ? runWithOwner(owner, () => createMemo(calc, undefined, options)) : calc; + return memo ? runWithOwner(owner, () => createMemo(calc, options)) : calc; }); } @@ -112,7 +112,7 @@ export function destructure { @@ -32,6 +32,7 @@ describe("destructure", () => { set_count(10); set_label("bar"); set_highlight(true); + flush(); expect(count()).toBe(10); expect(label()).toBe("bar"); @@ -40,249 +41,217 @@ describe("destructure", () => { dispose(); }); - test("spread array", () => - createRoot(dispose => { - const [numbers, setNumbers] = createSignal([1, 2, 3] as [number, number, number]); - const [first, second, last] = destructure(numbers); + test("spread array", () => { + const [numbers, setNumbers] = createSignal([1, 2, 3] as [number, number, number]); + const updates = { a: 0, b: 0, c: 0 }; - const updates = { - a: 0, - b: 0, - c: 0, - }; - createComputed(() => { + const { first, second, last, dispose } = createRoot(d => { + const [first, second, last] = destructure(numbers); + createTrackedEffect(() => { first(); updates.a++; }); - createComputed(() => { + createTrackedEffect(() => { second(); updates.b++; }); - createComputed(() => { + createTrackedEffect(() => { last(); updates.c++; }); + return { first, second, last, dispose: d }; + }); + flush(); - expect(first()).toBe(1); - expect(second()).toBe(2); - expect(last()).toBe(3); + expect(first()).toBe(1); + expect(second()).toBe(2); + expect(last()).toBe(3); - expect(updates.a).toBe(1); - expect(updates.b).toBe(1); - expect(updates.c).toBe(1); + expect(updates.a).toBe(1); + expect(updates.b).toBe(1); + expect(updates.c).toBe(1); - setNumbers([1, 6, 7]); - expect(first()).toBe(1); - expect(second()).toBe(6); - expect(last()).toBe(7); + setNumbers([1, 6, 7]); + flush(); + expect(first()).toBe(1); + expect(second()).toBe(6); + expect(last()).toBe(7); - expect(updates.a).toBe(1); - expect(updates.b).toBe(2); - expect(updates.c).toBe(2); + expect(updates.a).toBe(1); + expect(updates.b).toBe(2); + expect(updates.c).toBe(2); + + dispose(); + }); - dispose(); - })); + test("spread object", () => { + const [numbers, setNumbers] = createSignal({ a: 1, b: 2, c: 3 }); + const updates = { a: 0, b: 0, c: 0 }; - test("spread object", () => - createRoot(dispose => { - const [numbers, setNumbers] = createSignal({ - a: 1, - b: 2, - c: 3, - }); + const { a, b, c, dispose } = createRoot(d => { const { a, b, c } = destructure(numbers); - - const updates = { - a: 0, - b: 0, - c: 0, - }; - createComputed(() => { + createTrackedEffect(() => { a(); updates.a++; }); - createComputed(() => { + createTrackedEffect(() => { b(); updates.b++; }); - createComputed(() => { + createTrackedEffect(() => { c(); updates.c++; }); + return { a, b, c, dispose: d }; + }); + flush(); - expect(a()).toBe(1); - expect(b()).toBe(2); - expect(c()).toBe(3); + expect(a()).toBe(1); + expect(b()).toBe(2); + expect(c()).toBe(3); - expect(updates.a).toBe(1); - expect(updates.b).toBe(1); - expect(updates.c).toBe(1); + expect(updates.a).toBe(1); + expect(updates.b).toBe(1); + expect(updates.c).toBe(1); - setNumbers({ - a: 1, - b: 6, - c: 7, - }); - expect(a()).toBe(1); - expect(b()).toBe(6); - expect(c()).toBe(7); + setNumbers({ a: 1, b: 6, c: 7 }); + flush(); + expect(a()).toBe(1); + expect(b()).toBe(6); + expect(c()).toBe(7); - expect(updates.a).toBe(1); - expect(updates.b).toBe(2); - expect(updates.c).toBe(2); + expect(updates.a).toBe(1); + expect(updates.b).toBe(2); + expect(updates.c).toBe(2); - dispose(); - })); + dispose(); + }); - test("spread is eager", () => - createRoot(dispose => { - const [numbers, setNumbers] = createSignal<{ a: number; b?: number }>({ - a: 0, - }); + test("spread is eager", () => { + const [numbers, setNumbers] = createSignal<{ a: number; b?: number }>({ a: 0 }); + const { a, b, dispose } = createRoot(d => { const { a, b } = destructure(numbers); + return { a, b, dispose: d }; + }); - expect(a()).toBe(0); - expect(b).toBe(undefined); + expect(a()).toBe(0); + expect(b).toBe(undefined); - setNumbers({ - a: 2, - b: 3, - }); + setNumbers({ a: 2, b: 3 }); + flush(); - expect(a()).toBe(2); - expect(b).toBe(undefined); + expect(a()).toBe(2); + expect(b).toBe(undefined); - dispose(); - })); + dispose(); + }); - test("destructure object", () => - createRoot(dispose => { - const [numbers, setNumbers] = createSignal({ - a: 1, - b: 2, - c: 3, - }); - const { a, b, c } = destructure(numbers, { lazy: true }); + test("destructure object", () => { + const [numbers, setNumbers] = createSignal({ a: 1, b: 2, c: 3 }); + const updates = { a: 0, b: 0, c: 0 }; - const updates = { - a: 0, - b: 0, - c: 0, - }; - createComputed(() => { + const { a, b, c, dispose } = createRoot(d => { + const { a, b, c } = destructure(numbers, { lazy: true }); + createTrackedEffect(() => { a(); updates.a++; }); - createComputed(() => { + createTrackedEffect(() => { b(); updates.b++; }); - createComputed(() => { + createTrackedEffect(() => { c(); updates.c++; }); + return { a, b, c, dispose: d }; + }); + flush(); - expect(a()).toBe(1); - expect(b()).toBe(2); - expect(c()).toBe(3); + expect(a()).toBe(1); + expect(b()).toBe(2); + expect(c()).toBe(3); - expect(updates.a).toBe(1); - expect(updates.b).toBe(1); - expect(updates.c).toBe(1); + expect(updates.a).toBe(1); + expect(updates.b).toBe(1); + expect(updates.c).toBe(1); - setNumbers({ - a: 1, - b: 6, - c: 7, - }); - expect(a()).toBe(1); - expect(b()).toBe(6); - expect(c()).toBe(7); + setNumbers({ a: 1, b: 6, c: 7 }); + flush(); + expect(a()).toBe(1); + expect(b()).toBe(6); + expect(c()).toBe(7); - expect(updates.a).toBe(1); - expect(updates.b).toBe(2); - expect(updates.c).toBe(2); + expect(updates.a).toBe(1); + expect(updates.b).toBe(2); + expect(updates.c).toBe(2); - dispose(); - })); + dispose(); + }); - test("destructure is lazy", () => - createRoot(dispose => { - const [numbers, setNumbers] = createSignal<{ a: number; b?: number }>({ - a: 0, - }); + test("destructure is lazy", () => { + const [numbers, setNumbers] = createSignal<{ a: number; b?: number }>({ a: 0 }); + const { a, b, dispose } = createRoot(d => { const { a, b } = destructure(numbers, { lazy: true }); + return { a, b, dispose: d }; + }); - expect(a()).toBe(0); - expect(b()).toBe(undefined); + expect(a()).toBe(0); + expect(b()).toBe(undefined); - setNumbers({ - a: 2, - b: 3, - }); + setNumbers({ a: 2, b: 3 }); + flush(); - expect(a()).toBe(2); - expect(b()).toBe(3); + expect(a()).toBe(2); + expect(b()).toBe(3); - dispose(); - })); + dispose(); + }); - test("destructure recursively nested objects", () => - createRoot(dispose => { - const [numbers, setNumbers] = createSignal({ - nested: { - a: 1, - b: 2, - c: 3, - }, - }); + test("destructure recursively nested objects", () => { + const [numbers, setNumbers] = createSignal({ nested: { a: 1, b: 2, c: 3 } }); + const updates = { a: 0, b: 0, c: 0 }; + + const { a, b, c, dispose } = createRoot(d => { const { nested: { a, b, c }, } = destructure(numbers, { deep: true }); - - const updates = { - a: 0, - b: 0, - c: 0, - }; - createComputed(() => { + createTrackedEffect(() => { a(); updates.a++; }); - createComputed(() => { + createTrackedEffect(() => { b(); updates.b++; }); - createComputed(() => { + createTrackedEffect(() => { c(); updates.c++; }); + return { a, b, c, dispose: d }; + }); + flush(); - expect(a()).toBe(1); - expect(b()).toBe(2); - expect(c()).toBe(3); + expect(a()).toBe(1); + expect(b()).toBe(2); + expect(c()).toBe(3); - expect(updates.a).toBe(1); - expect(updates.b).toBe(1); - expect(updates.c).toBe(1); + expect(updates.a).toBe(1); + expect(updates.b).toBe(1); + expect(updates.c).toBe(1); - setNumbers({ - nested: { - a: 1, - b: 6, - c: 7, - }, - }); - expect(a()).toBe(1); - expect(b()).toBe(6); - expect(c()).toBe(7); + setNumbers({ nested: { a: 1, b: 6, c: 7 } }); + flush(); + expect(a()).toBe(1); + expect(b()).toBe(6); + expect(c()).toBe(7); - expect(updates.a).toBe(1); - expect(updates.b).toBe(2); - expect(updates.c).toBe(2); + expect(updates.a).toBe(1); + expect(updates.b).toBe(2); + expect(updates.c).toBe(2); - dispose(); - })); + dispose(); + }); }); diff --git a/packages/memo/README.md b/packages/memo/README.md index d3e5bdb6a..99d23babc 100644 --- a/packages/memo/README.md +++ b/packages/memo/README.md @@ -152,21 +152,11 @@ memo(); // T: number ### No `equals` -The lazy memo, as it is implemented now, doesn't allow for setting a `equals` function like you could with normal memo. It is always set to `equals: false` as a lazy memo cannot eagerly evaluate to check if the next value is the same as the previous, it needs to always notify it's observers so they can read from it and evaluate the memo. +`createLazyMemo` always uses `equals: false` internally. A lazy memo cannot eagerly evaluate to check equality — it needs to always notify its observers so they can pull and evaluate the new value. -### Not ownerless +### Auto-disposal and recomputation -Lazy memos in Solid 2.0 will be ownerless — the reactive context of the callback will depend of the place of read, not creation. - -This implementation will always execute it's callback with the context of owner it was created under. So ti won't work with [Suspense](https://www.solidjs.com/docs/latest/api#) the way you might expect — meaning that it won't activate any Suspense that is below place of creation. - -Although if you only need the ownerless characteristics so that the memo can be garbage-collected when not referenced, instead of waiting for owner cleanup, you can wrap it with [`runWithOwner`](https://www.solidjs.com/docs/latest/api#runwithowner) to create it without an owner: - -```ts -const memo = runWithOwner(null, () => { - return createLazyMemo(() => /* ... */) -}) -``` +In Solid 2.0, `createLazyMemo` is backed by a native `createMemo({ lazy: true })`. When all observers disconnect the memo auto-disposes, and the next observer that reads it triggers a fresh computation from scratch. This means the previous value is **not** retained across idle periods — `prev` will be `undefined` again after a full unsubscribe/resubscribe cycle. ### You may not need a lazy memo @@ -185,33 +175,15 @@ https://codesandbox.io/s/solid-primitives-memo-demo-3w0oz?file=/index.tsx ## `createDebouncedMemo` -`createDebouncedMemo` is deprecated. Please use `createSchedule` from [`@solid-primitives/schedule`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#readme) instead. - -```ts -import { createSchedule, debounce } from "@solid-primitives/schedule"; - -const scheduled = createScheduled(fn => debounce(fn, 200)); - -const double = createMemo(p => { - const value = count(); - return scheduled() ? value * 2 : p; -}, 0); -``` +**Removed in v2.** `createDebouncedMemo` was deprecated in v1 and has been removed. Use `createScheduled` from [`@solid-primitives/schedule`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#readme) once that package is updated for Solid 2.0. ## `createThrottledMemo` -`createThrottledMemo` is deprecated. Please use `createSchedule` from [`@solid-primitives/schedule`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#readme) instead. +**Removed in v2.** `createThrottledMemo` was deprecated in v1 and has been removed. Use `createScheduled` from [`@solid-primitives/schedule`](https://github.com/solidjs-community/solid-primitives/tree/main/packages/scheduled#readme) once that package is updated for Solid 2.0. -```ts -import { createSchedule, throttle } from "@solid-primitives/schedule"; - -const scheduled = createScheduled(fn => throttle(fn, 200)); +## `createAsyncMemo` -const double = createMemo(p => { - const value = count(); - return scheduled() ? value * 2 : p; -}, 0); -``` +**Removed in v2.** `createAsyncMemo` was deprecated in v1 (pointing to `createResource`) and has been removed. `createResource` does not exist in Solid 2.0. Async data fetching in Solid 2.0 can be handled with `createProjection` for store-based async state, or with a signal + `createEffect` for manual async management. ## `createPureReaction` diff --git a/packages/memo/package.json b/packages/memo/package.json index 69f8afd59..f4d7c368b 100644 --- a/packages/memo/package.json +++ b/packages/memo/package.json @@ -56,16 +56,15 @@ "memo" ], "dependencies": { - "@solid-primitives/scheduled": "workspace:^", "@solid-primitives/utils": "workspace:^" }, "devDependencies": { - "@solid-primitives/mouse": "workspace:^", - "@solidjs/router": "^0.8.4", - "solid-js": "^1.9.7" + "@solidjs/web": "2.0.0-beta.10", + "solid-js": "2.0.0-beta.10" }, "peerDependencies": { - "solid-js": "^1.6.12" + "@solidjs/web": "^2.0.0-beta.10", + "solid-js": "^2.0.0-beta.10" }, "typesVersions": {} } diff --git a/packages/memo/src/index.ts b/packages/memo/src/index.ts index 1219bb3d5..64d13cfd4 100644 --- a/packages/memo/src/index.ts +++ b/packages/memo/src/index.ts @@ -1,29 +1,22 @@ import { type Accessor, createSignal, - createComputed, untrack, getOwner, onCleanup, createMemo, + createReaction, runWithOwner, type Setter, - on, - createRoot, - type AccessorArray, - type EffectFunction, + type ComputeFunction, type MemoOptions, type NoInfer, type Owner, type SignalOptions, DEV, } from "solid-js"; -import { isServer } from "solid-js/web"; -import { debounce, throttle } from "@solid-primitives/scheduled"; -import { noop, type EffectOptions, EQUALS_FALSE_OPTIONS } from "@solid-primitives/utils"; - -export type MemoOptionsWithValue = MemoOptions & { value?: T }; -export type AsyncMemoCalculation = (prev: T | Init) => Promise | T; +import { isServer } from "@solidjs/web"; +import { type EffectOptions, EQUALS_FALSE_OPTIONS } from "@solid-primitives/utils"; const callbackWith = (fn: (a: A) => T, v: Accessor): (() => T) => fn.length > 0 ? () => fn(untrack(v)) : (fn as () => T); @@ -56,12 +49,9 @@ export function createPureReaction( } const owner = getOwner()!; - const disposers = new Set(); let trackers = 0; let disposed = false; onCleanup(() => { - for (const fn of disposers) fn(); - disposers.clear(); disposed = true; }); @@ -72,19 +62,12 @@ export function createPureReaction( return; } trackers++; - createRoot(dispose => { - disposers.add(dispose); - let init = true; - createComputed(() => { - if (init) { - init = false; - return tracking(); - } + const r = runWithOwner(owner, () => + createReaction(() => { if (--trackers === 0) untrack(onInvalidate); - dispose(); - disposers.delete(dispose); - }, options); - }, owner); + }, options), + ); + r(tracking); }; } @@ -109,11 +92,10 @@ export function createLatest[]>( const memos = sources.map((source, i) => createMemo( () => ((index = i), source()), - undefined, DEV ? { name: i + 1 + ". source", equals: false } : EQUALS_FALSE_OPTIONS, ), ); - return createMemo(() => memos.map(m => m())[index]!, undefined, options); + return createMemo(() => memos.map(m => m())[index]!, options); } /** @@ -131,18 +113,17 @@ export function createLatest[]>( */ export function createLatestMany[]>( sources: T, - options?: EffectOptions, + options?: MemoOptions[]>, ): Accessor[]>; export function createLatestMany( sources: readonly Accessor[], - options?: EffectOptions, + options?: MemoOptions, ): Accessor { const memos = sources.map((source, i) => { const obj = { dirty: true, get: null as any as Accessor }; obj.get = createMemo( () => ((obj.dirty = true), source()), - undefined, DEV ? { name: i + 1 + ". source", equals: false } : EQUALS_FALSE_OPTIONS, ); @@ -178,10 +159,10 @@ export function createLatestMany( * setResult(5) // overwrites calculation result */ export function createWritableMemo( - fn: EffectFunction, Next>, + fn: ComputeFunction, Next>, ): [signal: Accessor, setter: Setter]; export function createWritableMemo( - fn: EffectFunction, + fn: ComputeFunction, value: Init, options?: MemoOptions, ): [signal: Accessor, setter: Setter]; @@ -192,10 +173,9 @@ export function createWritableMemo( ): [signal: Accessor, setter: Setter] { let combined: Accessor = () => value as T; - const [signal, setSignal] = createSignal(value as T, EQUALS_FALSE_OPTIONS), + const [signal, setSignal] = createSignal(value as Exclude, { equals: false, ownedWrite: true }), memo = createMemo( callbackWith(fn, () => combined()), - value, EQUALS_FALSE_OPTIONS, ); @@ -208,161 +188,6 @@ export function createWritableMemo( ]; } -/** - * @deprecated Please use `createSchedule` from `@solid-primitives/schedule` instead. - */ -export function createDebouncedMemo( - fn: EffectFunction, Next>, - timeoutMs: number, -): Accessor; -export function createDebouncedMemo( - fn: EffectFunction, - timeoutMs: number, - value: Init, - options?: MemoOptions, -): Accessor; -export function createDebouncedMemo( - fn: (prev: T | undefined) => T, - timeoutMs: number, - value?: T, - options?: MemoOptions, -): Accessor { - const memo = createMemo(() => fn(value), undefined, options); - if (isServer) { - return memo; - } - const [signal, setSignal] = createSignal(untrack(memo)); - const updateSignal = debounce(() => (value = setSignal(memo)), timeoutMs); - createComputed(on(memo, updateSignal, { defer: true })); - return signal; -} - -/** - * @deprecated Please use `createSchedule` from `@solid-primitives/schedule` instead. - */ -export function createDebouncedMemoOn( - deps: AccessorArray | Accessor, - fn: (input: S, prevInput: S | undefined, prev: undefined | NoInfer) => Next, - timeoutMs: number, -): Accessor; -export function createDebouncedMemoOn( - deps: AccessorArray | Accessor, - fn: (input: S, prevInput: S | undefined, prev: Prev | Init) => Next, - timeoutMs: number, - value: Init, - options?: MemoOptions, -): Accessor; -export function createDebouncedMemoOn( - deps: AccessorArray | Accessor, - fn: (input: S, prevInput: S | undefined, prev: Prev | undefined) => Next, - timeoutMs: number, - value?: Prev, - options?: MemoOptions, -): Accessor { - if (isServer) { - return createMemo(on(deps, fn as any) as () => any, value); - } - let init = true; - const [signal, setSignal] = createSignal( - (() => { - let v!: Next; - createComputed( - on(deps, (input, prevInput) => { - if (init) { - v = fn(input, prevInput, value); - init = false; - } else updateSignal(input, prevInput); - }), - ); - return v; - })(), - options, - ); - const updateSignal = debounce((input: S, prevInput: S | undefined) => { - setSignal(() => fn(input, prevInput, signal())); - }, timeoutMs); - return signal; -} - -/** - * @deprecated Please use `createSchedule` from `@solid-primitives/schedule` instead. - */ -export function createThrottledMemo( - fn: EffectFunction, Next>, - timeoutMs: number, -): Accessor; -export function createThrottledMemo( - fn: EffectFunction, - timeoutMs: number, - value: Init, - options?: MemoOptions, -): Accessor; -export function createThrottledMemo( - fn: (prev: T | undefined) => T, - timeoutMs: number, - value?: T, - options?: MemoOptions, -): Accessor { - if (isServer) { - return createMemo(fn); - } - let onInvalidate: VoidFunction = noop; - const track = createPureReaction(() => onInvalidate()); - const [state, setState] = createSignal( - (() => { - let v!: T; - track(() => (v = fn(value))); - return v; - })(), - options, - ); - onInvalidate = throttle(() => track(() => setState(fn)), timeoutMs); - return state; -} - -/** - * @deprecated Please just use `createResource` instead. - */ -export function createAsyncMemo( - calc: AsyncMemoCalculation, - options: MemoOptionsWithValue & { value: T }, -): Accessor; -export function createAsyncMemo( - calc: AsyncMemoCalculation, - options?: MemoOptionsWithValue, -): Accessor; -export function createAsyncMemo( - calc: AsyncMemoCalculation, - options: MemoOptionsWithValue = {}, -): Accessor { - if (isServer) { - return () => options.value; - } - const [state, setState] = createSignal(options.value, options); - /** pending promises from oldest to newest */ - const order: Promise[] = []; - - // prettier-ignore - createComputed(async () => { - const value = calc(untrack(state)); - if (value instanceof Promise) { - order.push(value); - // resolved value will only be written to the signal, - // if the promise wasn't removed from the array - value.then(r => order.includes(value) && setState(() => r)) - // when a promise finishes, it removes itself, and every older promise from array, - // blocking them from overwriting the state if they finish after - value.finally(() => { - const index = order.indexOf(value); - order.splice(0, index + 1); - }); - } - else setState(() => value); - }, undefined, options); - - return state; -} - /** * Lazily evaluated `createMemo`. Will run the calculation only if is being listened to. * @@ -379,19 +204,19 @@ export function createAsyncMemo( export function createLazyMemo( calc: (prev: T) => T, value: T, - options?: EffectOptions, + options?: MemoOptions, ): Accessor; export function createLazyMemo( calc: (prev: T | undefined) => T, value?: undefined, - options?: EffectOptions, + options?: MemoOptions, ): Accessor; export function createLazyMemo( calc: (prev: T | undefined) => T, value?: T, - options?: EffectOptions, + options?: MemoOptions, ): Accessor { if (isServer) { let calculated = false; @@ -404,23 +229,15 @@ export function createLazyMemo( }; } - let isReading = false, - isStale: boolean | undefined = true; - - const [track, trigger] = createSignal(void 0, EQUALS_FALSE_OPTIONS), - memo = createMemo( - p => (isReading ? calc(p) : ((isStale = !track()), p)), - value as T, - DEV ? { name: options?.name, equals: false } : EQUALS_FALSE_OPTIONS, - ); + let prevValue: T | undefined = value; - return (): T => { - isReading = true; - if (isStale) isStale = trigger(); - const v = memo(); - isReading = false; - return v; - }; + return createMemo( + (): T => { + prevValue = calc(prevValue); + return prevValue; + }, + DEV ? { lazy: true, name: options?.name, equals: false } : { lazy: true, equals: false }, + ); } export type CacheCalculation = (key: Key, prev: Value | undefined) => Value; @@ -475,9 +292,14 @@ export function createMemoCache( const run: CacheKeyAccessor = key => { if (cache.has(key)) return (cache.get(key) as Accessor)(); + let prevVal: Value | undefined; const memo = runWithOwner(owner, () => - createLazyMemo(prev => calc(key, prev), undefined, options), - )!; + createMemo((): Value => { + const v = calc(key, prevVal); + prevVal = v; + return v; + }, options), + ); if (options.size === undefined || cache.size < options.size) cache.set(key, memo); return memo(); }; @@ -502,7 +324,7 @@ export function createReducer>( initialValue: T, options?: SignalOptions, ): [accessor: Accessor, dispatch: (...args: ActionData) => void] { - const [state, setState] = createSignal(initialValue, options); + const [state, setState] = createSignal(initialValue as Exclude, { ownedWrite: true, ...options }); return [state, (...args: ActionData) => void setState(state => dispatcher(state, ...args))]; } diff --git a/packages/memo/test/async.test.ts b/packages/memo/test/async.test.ts deleted file mode 100644 index a60752960..000000000 --- a/packages/memo/test/async.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, test, expect } from "vitest"; -import { createRoot, createSignal } from "solid-js"; -import { createAsyncMemo } from "../src/index.js"; - -describe("createAsyncMemo", () => { - test("resolves synchronous functions", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - const memo = createAsyncMemo(count); - expect(count()).toBe(memo()); - setCount(1); - expect(count()).toBe(memo()); - dispose(); - })); - - test("resolves asynchronous functions", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - const memo = createAsyncMemo( - () => - new Promise(res => { - const n = count(); - setTimeout(() => res(n), 0); - }), - ); - expect(memo()).toBe(undefined); - setCount(1); - expect(memo()).toBe(undefined); - setTimeout(() => { - expect(count()).toBe(memo()); - dispose(); - }, 0); - })); - - test("preserves order of execution", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let first = true; - const memo = createAsyncMemo( - () => - new Promise(res => { - const n = count(); - if (first) { - first = false; - setTimeout(() => res(n), 100); - } else { - setTimeout(() => res(n), 0); - } - }), - ); - setCount(1); - setTimeout(() => { - expect(memo()).toBe(1); - dispose(); - }, 100); - })); -}); diff --git a/packages/memo/test/cache.test.ts b/packages/memo/test/cache.test.ts index b54adb552..7bae4e04d 100644 --- a/packages/memo/test/cache.test.ts +++ b/packages/memo/test/cache.test.ts @@ -1,39 +1,43 @@ import { describe, test, expect } from "vitest"; -import { createRoot, createSignal } from "solid-js"; +import { createRoot, createSignal, flush } from "solid-js"; import { createMemoCache } from "../src/index.js"; describe("createMemoCache", () => { - test("cashes values by key", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - - let runs = 0; - const result = createMemoCache(count, n => { + test("cashes values by key", () => { + const [count, setCount] = createSignal(0); + let runs = 0; + const { result, dispose } = createRoot(d => ({ + result: createMemoCache(count, n => { runs++; return n; - }); + }), + dispose: d, + })); - expect(runs).toBe(0); - expect(result()).toBe(0); - expect(runs).toBe(1); + expect(runs).toBe(0); + expect(result()).toBe(0); + expect(runs).toBe(1); - setCount(1); - expect(runs).toBe(1); - expect(result()).toBe(1); - expect(runs).toBe(2); + setCount(1); + flush(); + expect(runs).toBe(1); + expect(result()).toBe(1); + expect(runs).toBe(2); - setCount(0); - expect(runs).toBe(2); - expect(result()).toBe(0); - expect(runs).toBe(2); + setCount(0); + flush(); + expect(runs).toBe(2); + expect(result()).toBe(0); + expect(runs).toBe(2); - setCount(1); - expect(runs).toBe(2); - expect(result()).toBe(1); - expect(runs).toBe(2); + setCount(1); + flush(); + expect(runs).toBe(2); + expect(result()).toBe(1); + expect(runs).toBe(2); - dispose(); - })); + dispose(); + }); test("passing key to access function", () => createRoot(dispose => { @@ -59,37 +63,39 @@ describe("createMemoCache", () => { dispose(); })); - test("reactive signal dependency", () => - createRoot(dispose => { - const [dep, setDep] = createSignal(0); - - let runs = 0; - const result = createMemoCache((n: number) => { + test("reactive signal dependency", () => { + const [dep, setDep] = createSignal(0); + let runs = 0; + const { result, dispose } = createRoot(d => ({ + result: createMemoCache((n: number) => { runs++; return n + dep(); - }); + }), + dispose: d, + })); - expect(runs).toBe(0); - expect(result(0)).toBe(0); - expect(runs).toBe(1); + expect(runs).toBe(0); + expect(result(0)).toBe(0); + expect(runs).toBe(1); - expect(result(1)).toBe(1); - expect(runs).toBe(2); + expect(result(1)).toBe(1); + expect(runs).toBe(2); - expect(result(0)).toBe(0); - expect(runs).toBe(2); + expect(result(0)).toBe(0); + expect(runs).toBe(2); - expect(result(1)).toBe(1); - expect(runs).toBe(2); + expect(result(1)).toBe(1); + expect(runs).toBe(2); - setDep(1); - expect(runs).toBe(2); - expect(result(0)).toBe(1); - expect(result(1)).toBe(2); - expect(runs).toBe(4); + setDep(1); + expect(runs).toBe(2); + flush(); + expect(result(0)).toBe(1); + expect(result(1)).toBe(2); + expect(runs).toBe(4); - dispose(); - })); + dispose(); + }); test("limit cache size", () => createRoot(dispose => { diff --git a/packages/memo/test/latest.test.ts b/packages/memo/test/latest.test.ts index ff5eda811..cbb1c89a4 100644 --- a/packages/memo/test/latest.test.ts +++ b/packages/memo/test/latest.test.ts @@ -1,31 +1,44 @@ import { describe, test, expect } from "vitest"; -import { batch, createEffect, createRoot, createSignal } from "solid-js"; +import { createEffect, createRoot, createSignal, flush } from "solid-js"; import { createLatest, createLatestMany } from "../src/index.js"; describe("createLatest", () => { test("should return the latest value", () => { - createRoot(dispose => { - const [a, setA] = createSignal({ a: 1 }); - const [b, setB] = createSignal({ b: 2 }); - const latest = createLatest([a, b]); - expect(latest()).toEqual({ b: 2 }); - setA({ a: 3 }); - expect(latest()).toEqual({ a: 3 }); - setB({ b: 4 }); - expect(latest()).toEqual({ b: 4 }); - setB({ b: 5 }); - setA({ a: 6 }); - expect(latest()).toEqual({ a: 6 }); - setB({ b: 7 }); - expect(latest()).toEqual({ b: 7 }); - batch(() => { - setB({ b: 8 }); - setA({ a: 9 }); - expect(latest()).toEqual({ b: 8 }); - }); - expect(latest()).toEqual({ b: 8 }); - dispose(); - }); + const [a, setA] = createSignal({ a: 1 }); + const [b, setB] = createSignal({ b: 2 }); + const { latest, dispose } = createRoot(d => ({ + latest: createLatest([a, b]), + dispose: d, + })); + + flush(); + expect(latest()).toEqual({ b: 2 }); + + setA({ a: 3 }); + flush(); + expect(latest()).toEqual({ a: 3 }); + + setB({ b: 4 }); + flush(); + expect(latest()).toEqual({ b: 4 }); + + // when both updated in the same tick, last write wins + setB({ b: 5 }); + setA({ a: 6 }); + flush(); + expect(latest()).toEqual({ a: 6 }); + + setB({ b: 7 }); + flush(); + expect(latest()).toEqual({ b: 7 }); + + // consecutive updates — last write wins + setB({ b: 8 }); + setA({ a: 9 }); + flush(); + expect(latest()).toEqual({ a: 9 }); + + dispose(); }); test("works with equals: false sources", () => { @@ -33,26 +46,33 @@ describe("createLatest", () => { const [b, setB] = createSignal("b"); let captured: any; - const dispose = createRoot(dispose => { + const dispose = createRoot(d => { const latest = createLatest([a, b]); - createEffect(() => { - captured = latest(); - }); - return dispose; + createEffect( + () => latest(), + value => { + captured = value; + }, + ); + return d; }); + flush(); expect(captured).toBe("b"); captured = undefined; setA(1); + flush(); expect(captured).toBe(1); captured = undefined; setB("c"); + flush(); expect(captured).toBe("c"); captured = undefined; setA(1); + flush(); expect(captured).toBe(1); dispose(); @@ -62,27 +82,30 @@ describe("createLatest", () => { const [a, setA] = createSignal(0); const [b, setB] = createSignal("b"); - const { latest, dispose } = createRoot(dispose => { - return { - latest: createLatest([a, b], { - equals: (a, b) => typeof b === "number", - }), - dispose, - }; - }); + const { latest, dispose } = createRoot(d => ({ + latest: createLatest([a, b], { + equals: (a, b) => typeof b === "number", + }), + dispose: d, + })); + flush(); expect(latest()).toBe("b"); setA(1); + flush(); expect(latest()).toBe("b"); setB("c"); + flush(); expect(latest()).toBe("c"); setA(2); + flush(); expect(latest()).toBe("c"); setB("d"); + flush(); expect(latest()).toBe("d"); dispose(); @@ -91,28 +114,41 @@ describe("createLatest", () => { describe("createLatestMany", () => { test("should return the latest values", () => { - createRoot(dispose => { - const [a, setA] = createSignal({ a: 1 }); - const [b, setB] = createSignal({ b: 2 }); - const latest = createLatestMany([a, b]); - expect(latest()).toEqual([{ a: 1 }, { b: 2 }]); - setA({ a: 3 }); - expect(latest()).toEqual([{ a: 3 }]); - setB({ b: 4 }); - expect(latest()).toEqual([{ b: 4 }]); - setB({ b: 5 }); - setA({ a: 6 }); - expect(latest()).toEqual([{ a: 6 }, { b: 5 }]); - setB({ b: 7 }); - expect(latest()).toEqual([{ b: 7 }]); - batch(() => { - setB({ b: 8 }); - setA({ a: 9 }); - expect(latest()).toEqual([{ a: 9 }, { b: 8 }]); - }); - expect(latest()).toEqual([{ a: 9 }, { b: 8 }]); - dispose(); - }); + const [a, setA] = createSignal({ a: 1 }); + const [b, setB] = createSignal({ b: 2 }); + const { latest, dispose } = createRoot(d => ({ + latest: createLatestMany([a, b]), + dispose: d, + })); + + // initial: both dirty + expect(latest()).toEqual([{ a: 1 }, { b: 2 }]); + + setA({ a: 3 }); + flush(); + expect(latest()).toEqual([{ a: 3 }]); + + setB({ b: 4 }); + flush(); + expect(latest()).toEqual([{ b: 4 }]); + + // both updated — both in array order + setB({ b: 5 }); + setA({ a: 6 }); + flush(); + expect(latest()).toEqual([{ a: 6 }, { b: 5 }]); + + setB({ b: 7 }); + flush(); + expect(latest()).toEqual([{ b: 7 }]); + + // consecutive updates — both show up in source order + setB({ b: 8 }); + setA({ a: 9 }); + flush(); + expect(latest()).toEqual([{ a: 9 }, { b: 8 }]); + + dispose(); }); test("works with equals: false sources", () => { @@ -120,26 +156,33 @@ describe("createLatestMany", () => { const [b, setB] = createSignal("b"); let captured: any; - const dispose = createRoot(dispose => { + const dispose = createRoot(d => { const latest = createLatestMany([a, b]); - createEffect(() => { - captured = latest(); - }); - return dispose; + createEffect( + () => latest(), + value => { + captured = value; + }, + ); + return d; }); + flush(); expect(captured).toEqual([0, "b"]); captured = undefined; setA(1); + flush(); expect(captured).toEqual([1]); captured = undefined; setB("c"); + flush(); expect(captured).toEqual(["c"]); captured = undefined; setA(1); + flush(); expect(captured).toEqual([1]); dispose(); diff --git a/packages/memo/test/lazy.test.ts b/packages/memo/test/lazy.test.ts index d9e9dc230..887bac3bd 100644 --- a/packages/memo/test/lazy.test.ts +++ b/packages/memo/test/lazy.test.ts @@ -1,261 +1,347 @@ import { describe, it, expect } from "vitest"; +import { flush } from "solid-js"; import { createLazyMemo } from "../src/index.js"; -import { createComputed, createEffect, createMemo, createRoot, createSignal } from "solid-js"; +import { createEffect, createMemo, createRoot, createSignal, createTrackedEffect } from "solid-js"; describe("createLazyMemo", () => { - it("won't run if not accessed", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let runs = 0; + it("won't run if not accessed", () => { + const [count, setCount] = createSignal(0); + let runs = 0; + const dispose = createRoot(d => { createLazyMemo(() => { runs++; return count(); }); - setCount(1); - expect(runs, "0 in setup").toBe(0); - - setTimeout(() => { - expect(runs, "0 after timeout").toBe(0); - setCount(2); - expect(runs, "0 after set in timeout").toBe(0); - dispose(); - }, 0); - })); - - it("runs after being accessed", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let runs = 0; - const memo = createLazyMemo(() => { + return d; + }); + + setCount(1); + expect(runs, "0 after setCount").toBe(0); + dispose(); + }); + + it("runs after being accessed", () => { + const [count, setCount] = createSignal(0); + let runs = 0; + let memo!: () => number; + const dispose = createRoot(d => { + memo = createLazyMemo(() => { runs++; return count(); }); - setCount(1); - expect(runs, "0 in setup").toBe(0); - - expect(memo(), "memo matches the signal on the first access").toBe(1); - expect(runs, "ran once").toBe(1); - dispose(); - })); - - it("runs only once, even if accessed multiple times", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let runs = 0; - const memo = createLazyMemo(() => { + return d; + }); + + setCount(1); + expect(runs, "0 before access").toBe(0); + + expect(memo(), "memo matches the signal on the first access").toBe(1); + expect(runs, "ran once").toBe(1); + dispose(); + }); + + it("runs only once per reactive update within a tracking context", () => { + const [count, setCount] = createSignal(0); + let runs = 0; + let memo!: () => number; + const dispose = createRoot(d => { + memo = createLazyMemo(() => { runs++; return count(); }); - setCount(1); - expect(runs, "0 in setup").toBe(0); - - expect(memo(), "memo matches the signal on the first access").toBe(1); - expect(memo(), "memo matches the signal on the second access").toBe(1); - expect(memo(), "memo matches the signal on the third access").toBe(1); - expect(runs, "ran once").toBe(1); - dispose(); - })); - - it("runs once until invalidated", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let runs = 0; - - const memo = createLazyMemo(() => { + return d; + }); + + setCount(1); + expect(runs, "0 before access").toBe(0); + + // Access within a reactive context — memo runs exactly once + let captured: number | undefined; + const innerDispose = createRoot(d => { + createTrackedEffect(() => { + captured = memo(); + memo(); + memo(); + }); + return d; + }); + flush(); + expect(captured).toBe(1); + expect(runs, "ran once inside reactive context").toBe(1); + + innerDispose(); + dispose(); + }); + + it("runs once until invalidated", () => { + const [count, setCount] = createSignal(0); + let runs = 0; + let memo!: () => number; + + const dispose = createRoot(d => { + memo = createLazyMemo(() => { runs++; return count(); }); + return d; + }); - createComputed(memo); - expect(runs, "1-1.").toBe(1); - createComputed(memo); - expect(runs, "1-2.").toBe(1); - memo(); - expect(runs, "1-3.").toBe(1); + const innerDispose1 = createRoot(d => { + createTrackedEffect(() => void memo()); + return d; + }); + flush(); + expect(runs, "1-1.").toBe(1); + + const innerDispose2 = createRoot(d => { + createTrackedEffect(() => void memo()); + return d; + }); + flush(); + expect(runs, "1-2.").toBe(1); + + memo(); + expect(runs, "1-3.").toBe(1); - setCount(1); - expect(runs, "2-1.").toBe(2); + setCount(1); + flush(); + expect(runs, "2-1.").toBe(2); - createComputed(memo); - expect(runs, "2-2.").toBe(2); + const innerDispose3 = createRoot(d => { + createTrackedEffect(() => void memo()); + return d; + }); + flush(); + expect(runs, "2-2.").toBe(2); - dispose(); - })); + innerDispose1(); + innerDispose2(); + innerDispose3(); + dispose(); + }); - it("won't run if the root of where it was accessed is gone", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let runs = 0; + it("won't run if the root of where it was accessed is gone", () => { + const [count, setCount] = createSignal(0); + let runs = 0; + let memo!: () => number; - const memo = createLazyMemo(() => { + const dispose = createRoot(d => { + memo = createLazyMemo(() => { runs++; return count(); }); + return d; + }); - createRoot(dispose => { - createComputed(memo); - dispose(); - }); + createRoot(innerDispose => { + createTrackedEffect(() => void memo()); + flush(); + innerDispose(); + }); + + expect(runs, "1").toBe(1); - expect(runs, "1").toBe(1); + setCount(1); + flush(); + expect(runs, "2").toBe(1); + dispose(); + }); - setCount(1); - expect(runs, "2").toBe(1); - dispose(); - })); + it("will be running even if some of the reading roots are disposed", () => { + const [count, setCount] = createSignal(0); + let runs = 0; + let memo!: () => number; - it("will be running even if some of the reading roots are disposed", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let runs = 0; - const memo = createLazyMemo(() => { + const dispose = createRoot(d => { + memo = createLazyMemo(() => { runs++; return count(); }); + return d; + }); - const dispose1 = createRoot(dispose => { - createComputed(memo); - return dispose; - }); - const dispose2 = createRoot(dispose => { - createComputed(memo); - return dispose; - }); + const dispose1 = createRoot(d => { + createTrackedEffect(() => void memo()); + flush(); + return d; + }); + const dispose2 = createRoot(d => { + createTrackedEffect(() => void memo()); + flush(); + return d; + }); - expect(runs, "ran once").toBe(1); + expect(runs, "ran once").toBe(1); - setCount(1); + setCount(1); + flush(); + expect(runs, "ran twice").toBe(2); - expect(runs, "ran twice").toBe(2); + dispose1(); + setCount(2); + flush(); + expect(runs, "ran 3 times").toBe(3); - dispose1(); - setCount(2); - expect(runs, "ran 3 times").toBe(3); + dispose2(); - dispose2(); + setCount(3); + flush(); + expect(runs, "ran 3 times; (not changed)").toBe(3); + dispose(); + }); - setCount(3); - expect(runs, "ran 3 times; (not changed)").toBe(3); - dispose(); - })); + it("initial value if NOT set in options", () => { + const [count, setCount] = createSignal(0); + let capturedPrev: any; + const captured: any[] = []; - it("initial value if NOT set in options", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let capturedPrev: any; + const dispose = createRoot(d => { const memo = createLazyMemo(prev => { capturedPrev = prev; return count(); }); - const captured: any[] = []; + createTrackedEffect(() => { + captured.push(memo()); + }); + return d; + }); - createComputed(() => captured.push(memo())); - expect(captured).toEqual([0]); - expect(capturedPrev).toEqual(undefined); + flush(); + expect(captured).toEqual([0]); + expect(capturedPrev).toEqual(undefined); - setCount(1); - expect(captured).toEqual([0, 1]); - expect(capturedPrev).toEqual(0); + setCount(1); + flush(); + expect(captured).toEqual([0, 1]); + expect(capturedPrev).toEqual(0); - dispose(); - })); + dispose(); + }); + + it("initial value if set", () => { + const [count, setCount] = createSignal(0); + let capturedPrev: any; + const captured: any[] = []; - it("initial value if set", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let capturedPrev: any; + const dispose = createRoot(d => { const memo = createLazyMemo(prev => { capturedPrev = prev; return count(); }, 123); - const captured: any[] = []; + createTrackedEffect(() => { + captured.push(memo()); + }); + return d; + }); - createComputed(() => captured.push(memo())); - expect(captured).toEqual([0]); - expect(capturedPrev).toEqual(123); + flush(); + expect(captured).toEqual([0]); + expect(capturedPrev).toEqual(123); - setCount(1); - expect(captured).toEqual([0, 1]); - expect(capturedPrev).toEqual(0); + setCount(1); + flush(); + expect(captured).toEqual([0, 1]); + expect(capturedPrev).toEqual(0); - dispose(); - })); + dispose(); + }); - it("handles prev value properly", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); + it("handles prev value properly", () => { + const [count, setCount] = createSignal(0); + let capturedPrev: any; + let memo!: () => number; - let capturedPrev: any; - const memo = createLazyMemo(prev => { + const dispose = createRoot(d => { + memo = createLazyMemo(prev => { capturedPrev = prev; return count(); }); + return d; + }); - const dis1 = createRoot(dis => { - createComputed(memo); - return dis; - }); - expect(capturedPrev).toBe(undefined); + const dis1 = createRoot(d => { + createTrackedEffect(() => void memo()); + flush(); + return d; + }); + expect(capturedPrev).toBe(undefined); + + setCount(1); + flush(); + expect(capturedPrev).toBe(0); - setCount(1); - expect(capturedPrev).toBe(0); + dis1(); - dis1(); + setCount(2); + flush(); + expect(capturedPrev).toBe(0); + expect(memo()).toBe(2); + expect(capturedPrev).toBe(1); + dispose(); + }); - setCount(2); - expect(capturedPrev).toBe(0); - expect(memo()).toBe(2); - expect(capturedPrev).toBe(1); - dispose(); - })); + it("works in an effect", () => { + const [count, setCount] = createSignal(0); + const captured: number[] = []; - it("works in an effect", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); + const dispose = createRoot(d => { const memo = createLazyMemo(count); - const captured: number[] = []; - createEffect(() => captured.push(memo())); + createEffect( + () => memo(), + value => { + captured.push(value); + }, + ); + return d; + }); - queueMicrotask(() => { - expect(captured).toEqual([0]); + flush(); + expect(captured).toEqual([0]); - setCount(1); - queueMicrotask(() => { - expect(captured).toEqual([0, 1]); - dispose(); - }); - }); - })); + setCount(1); + flush(); + expect(captured).toEqual([0, 1]); + dispose(); + }); - it("computation will last until the source changes", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - let runs = 0; - const memo = createLazyMemo(() => { + it("recomputes when resubscribed after all observers disposed", () => { + const [count, setCount] = createSignal(0); + let runs = 0; + let memo!: () => number; + + const dispose = createRoot(d => { + memo = createLazyMemo(() => { runs++; return count(); }); + return d; + }); - createRoot(dispose => { - createComputed(memo); - dispose(); - }); + createRoot(innerDispose => { + createTrackedEffect(() => void memo()); + flush(); + innerDispose(); + }); - expect(runs).toBe(1); + expect(runs).toBe(1); - createRoot(dispose => { - createComputed(memo); - dispose(); - }); - - expect(runs).toBe(1); + // In beta.10, lazy memos lose their dep links when unobserved + // and recompute when a new observer subscribes + createRoot(innerDispose => { + createTrackedEffect(() => void memo()); + flush(); + innerDispose(); + }); - setCount(1); + expect(runs).toBe(2); - expect(runs).toBe(1); + setCount(1); + flush(); + // No observers, so no recompute + expect(runs).toBe(2); - dispose(); - })); + dispose(); + }); it("stays in sync when read in a memo", () => { const { dispose, setA, memo } = createRoot(dispose => { @@ -271,6 +357,7 @@ describe("createLazyMemo", () => { memo(); setA(2); + flush(); memo(); dispose(); diff --git a/packages/memo/test/pureReaction.test.ts b/packages/memo/test/pureReaction.test.ts index 88b96d152..300cb17e6 100644 --- a/packages/memo/test/pureReaction.test.ts +++ b/packages/memo/test/pureReaction.test.ts @@ -1,128 +1,155 @@ import { describe, test, expect } from "vitest"; import { createPureReaction } from "../src/index.js"; -import { createEffect, createMemo, createRoot, createSignal } from "solid-js"; +import { createMemo, createRoot, createSignal, flush } from "solid-js"; describe("createPureReaction", () => { - test("tracking works", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - - let runCount = 0; - const track = createPureReaction(() => runCount++); - - expect(runCount, "onInvalidate shouldn't run before tracking").toBe(0); - track(() => count()); - expect(runCount, "shouldn't run before setting value").toBe(0); - setCount(1); - expect(runCount, "should run after tracked signal has been changed").toBe(1); - setCount(3); - expect(runCount, "next change should be ignored").toBe(1); - track(() => count()); - expect(runCount, "track itself shouldn't trigger callback").toBe(1); - setCount(2); - expect(runCount).toBe(2); - setCount(4); - expect(runCount, "next change should be ignored").toBe(2); - - dispose(); - })); - - test("tracking multiple sources", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - const [count2, setCount2] = createSignal(0); - - let runCount = 0; - const track = createPureReaction(() => runCount++); - - track(() => [count(), count2()]); - expect(runCount, "no changes yet").toBe(0); - - setCount2(1); - expect(runCount, "reaction triggered").toBe(1); - - setCount(1); - expect(runCount, "count is not tracked anymore").toBe(1); - - dispose(); - })); - - test("tracking single source multiple times", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - - let runCount = 0; - const track = createPureReaction(() => runCount++); - - track(count); - track(count); - expect(runCount, "no changes yet").toBe(0); - - setCount(1); - expect(runCount, "reaction triggered").toBe(1); - - setCount(2); - expect(runCount, "count is not tracked anymore").toBe(1); - - dispose(); - })); - - test("inInvalidate callback doesn't track by default", () => - createRoot(dispose => { - const [count, setCount] = createSignal(0); - - let runCount = 0; - const track = createPureReaction(() => { + test("tracking works", () => { + const [count, setCount] = createSignal(0); + let runCount = 0; + let track!: (fn: () => void) => void; + + const dispose = createRoot(d => { + track = createPureReaction(() => runCount++); + return d; + }); + + expect(runCount, "onInvalidate shouldn't run before tracking").toBe(0); + track(() => count()); + expect(runCount, "shouldn't run before setting value").toBe(0); + setCount(1); + flush(); + expect(runCount, "should run after tracked signal has been changed").toBe(1); + setCount(3); + flush(); + expect(runCount, "next change should be ignored").toBe(1); + track(() => count()); + expect(runCount, "track itself shouldn't trigger callback").toBe(1); + setCount(2); + flush(); + expect(runCount).toBe(2); + setCount(4); + flush(); + expect(runCount, "next change should be ignored").toBe(2); + + dispose(); + }); + + test("tracking multiple sources", () => { + const [count, setCount] = createSignal(0); + const [count2, setCount2] = createSignal(0); + let runCount = 0; + let track!: (fn: () => void) => void; + + const dispose = createRoot(d => { + track = createPureReaction(() => runCount++); + return d; + }); + + track(() => [count(), count2()]); + expect(runCount, "no changes yet").toBe(0); + + setCount2(1); + flush(); + expect(runCount, "reaction triggered").toBe(1); + + setCount(1); + flush(); + expect(runCount, "count is not tracked anymore").toBe(1); + + dispose(); + }); + + test("tracking single source multiple times", () => { + const [count, setCount] = createSignal(0); + let runCount = 0; + let track!: (fn: () => void) => void; + + const dispose = createRoot(d => { + track = createPureReaction(() => runCount++); + return d; + }); + + track(count); + track(count); + expect(runCount, "no changes yet").toBe(0); + + setCount(1); + flush(); + expect(runCount, "reaction triggered").toBe(1); + + setCount(2); + flush(); + expect(runCount, "count is not tracked anymore").toBe(1); + + dispose(); + }); + + test("inInvalidate callback doesn't track by default", () => { + const [count, setCount] = createSignal(0); + let runCount = 0; + let track!: (fn: () => void) => void; + + const dispose = createRoot(d => { + track = createPureReaction(() => { count(); runCount++; }); - - setCount(1); - expect(runCount, "no changes yet").toBe(0); - - setCount(2); - expect(runCount, "sstill none").toBe(0); - - track(count); - setCount(3); - expect(runCount, "ran once").toBe(1); - - setCount(4); - expect(runCount, "still ran only once").toBe(1); - - dispose(); - })); - - test("dispose stops tracking", () => + return d; + }); + + setCount(1); + flush(); + expect(runCount, "no changes yet").toBe(0); + + setCount(2); + flush(); + expect(runCount, "still none").toBe(0); + + track(count); + setCount(3); + flush(); + expect(runCount, "ran once").toBe(1); + + setCount(4); + flush(); + expect(runCount, "still ran only once").toBe(1); + + dispose(); + }); + + test("dispose stops tracking", () => { + const [count, setCount] = createSignal(0); + let runCount = 0; + let track!: (fn: () => void) => void; + + const dispose = createRoot(d => { + track = createPureReaction(() => runCount++); + return d; + }); + + track(count); + dispose(); + setCount(1); + flush(); + expect(runCount, "no tracking after disposal").toBe(0); + + track(count); + setCount(2); + flush(); + expect(runCount, "2. no tracking after disposal").toBe(0); + }); + + test("executes tracked functions synchronously", () => createRoot(dispose => { - const [count, setCount] = createSignal(0); + const order: number[] = []; - let runCount = 0; - const track = createPureReaction(() => runCount++); + const track = createPureReaction(() => {}); + order.push(1); + track(() => order.push(2)); + order.push(3); + createMemo(() => order.push(4)); - track(count); + expect(order).toEqual([1, 2, 3, 4]); dispose(); - setCount(1); - expect(runCount, "no tracking after disposal").toBe(0); - - track(count); - setCount(2); - expect(runCount, "2. no tracking after disposal").toBe(0); - })); - - test("executes tracked functions synchronously even in batched effects", () => - createRoot(dispose => { - createEffect(() => { - const order: number[] = []; - - const track = createPureReaction(() => {}); - order.push(1); - track(() => order.push(2)); - order.push(3); - createMemo(() => order.push(4)); - - expect(order).toEqual([1, 2, 3, 4]); - dispose(); - }); })); }); diff --git a/packages/memo/test/reducer.test.ts b/packages/memo/test/reducer.test.ts index 74cc05e95..ea2c9e213 100644 --- a/packages/memo/test/reducer.test.ts +++ b/packages/memo/test/reducer.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest"; -import { createRoot } from "solid-js"; +import { createRoot, flush } from "solid-js"; import { createReducer } from "../src/index.js"; describe("createReducer", () => { @@ -17,6 +17,7 @@ describe("createReducer", () => { expectedCounter *= 2; } + flush(); expect(counter()).toEqual(expectedCounter); dispose(); diff --git a/packages/memo/test/writable.test.ts b/packages/memo/test/writable.test.ts index 2277971f9..e84de80cf 100644 --- a/packages/memo/test/writable.test.ts +++ b/packages/memo/test/writable.test.ts @@ -1,127 +1,159 @@ import { describe, test, expect } from "vitest"; import { createWritableMemo } from "../src/index.js"; -import { batch, createRoot, createSignal } from "solid-js"; +import { createRoot, createSignal, flush } from "solid-js"; describe("createWritableMemo", () => { - test("behaves like a memo", () => - createRoot(dispose => { - const [count, setCount] = createSignal(1); + test("behaves like a memo", () => { + const [count, setCount] = createSignal(1); + const result = createRoot(d => { const [result] = createWritableMemo(() => count() * 2); - expect(result()).toBe(count() * 2); - setCount(5); - expect(result()).toBe(count() * 2); - dispose(); - })); - - test("value can be overwritten", () => - createRoot(dispose => { - const [count, setCount] = createSignal(1); + return result; + }); + flush(); + expect(result()).toBe(2); + setCount(5); + flush(); + expect(result()).toBe(10); + }); + + test("value can be overwritten", () => { + const [count, setCount] = createSignal(1); + const { result, setResult, dispose } = createRoot(d => { const [result, setResult] = createWritableMemo(() => count() * 2); - expect(result()).toBe(count() * 2); - setResult(5); - expect(result()).toBe(5); - setCount(5); - expect(result()).toBe(count() * 2); - setCount(7); - setResult(3); - expect(result()).toBe(3); - dispose(); - })); + return { result, setResult, dispose: d }; + }); + flush(); + expect(result()).toBe(2); + setResult(5); + flush(); + expect(result()).toBe(5); + setCount(5); + flush(); + expect(result()).toBe(10); + setCount(7); + flush(); + setResult(3); + flush(); + expect(result()).toBe(3); + dispose(); + }); test("consistency of previous value in the callbacks", () => { const [count, setCount] = createSignal(1); let prevCb: number | undefined; - const { result, setResult, dispose } = createRoot(dispose => { + const { result, setResult, dispose } = createRoot(d => { const [result, setResult] = createWritableMemo( (p?: number) => ((prevCb = p), count() * 2), -2, ); - expect(prevCb).toBe(-2); - expect(result()).toBe(2); - return { result, dispose, setResult }; + return { result, dispose: d, setResult }; }); + flush(); + expect(prevCb).toBe(-2); + expect(result()).toBe(2); + setResult(p => { expect(p).toBe(2); return 5; }); + flush(); expect(result()).toBe(5); expect(prevCb).toBe(-2); + setCount(5); - expect(result()).toBe(count() * 2); + flush(); + expect(result()).toBe(10); expect(prevCb).toBe(5); + setCount(7); + flush(); expect(prevCb).toBe(10); setResult(p => { expect(p).toBe(14); return 3; }); + flush(); expect(result()).toBe(3); dispose(); }); - test("updating and reading in a batch", () => { - createRoot(dispose => { - const [source, setSource] = createSignal(1); + test("updating and reading consecutively", () => { + const [source, setSource] = createSignal(1); + const { memo, setMemo, dispose } = createRoot(d => { const [memo, setMemo] = createWritableMemo(source, -2); + return { memo, setMemo, dispose: d }; + }); - batch(() => { - setSource(2); - expect(memo()).toBe(2); - }); + setSource(2); + flush(); + expect(memo()).toBe(2); - batch(() => { - setMemo(-3); - expect(memo()).toBe(-3); - }); + setMemo(-3); + flush(); + expect(memo()).toBe(-3); - dispose(); - }); + dispose(); }); test("return value of setter equals the new value", () => { - createRoot(dispose => { - const [source] = createSignal(1); + const [source] = createSignal(1); + const { memo, setMemo, dispose } = createRoot(d => { const [memo, setMemo] = createWritableMemo(source, -2); + return { memo, setMemo, dispose: d }; + }); - expect(setMemo(5)).toBe(5); - expect(memo()).toBe(5); - expect(setMemo(v => v + 1)).toBe(6); + flush(); + expect(setMemo(5)).toBe(5); + flush(); + expect(memo()).toBe(5); + expect(setMemo(v => v + 1)).toBe(6); + flush(); - dispose(); - }); + dispose(); }); - test("value can be overwritten twice to the same value", () => - createRoot(dispose => { - const [source, setSource] = createSignal(1); + test("value can be overwritten twice to the same value", () => { + const [source, setSource] = createSignal(1); + const { memo, setMemo, dispose } = createRoot(d => { const [memo, setMemo] = createWritableMemo(source, -2); - expect(memo()).toBe(source()); - setMemo(-2); - expect(memo()).toBe(-2); - setSource(5); - expect(memo()).toBe(source()); - setMemo(-2); - expect(memo()).toBe(-2); - dispose(); - })); + return { memo, setMemo, dispose: d }; + }); + + flush(); + expect(memo()).toBe(1); + setMemo(-2); + flush(); + expect(memo()).toBe(-2); + setSource(5); + flush(); + expect(memo()).toBe(5); + setMemo(-2); + flush(); + expect(memo()).toBe(-2); + dispose(); + }); // https://github.com/solidjs-community/solid-primitives/issues/772 test("issue 772", () => { const [source, setSource] = createSignal(0); - const [[value, setValue], dispose] = createRoot(dispose => [ - createWritableMemo(() => !!source()), - dispose, - ]); + const { value, setValue, dispose } = createRoot(d => { + const [value, setValue] = createWritableMemo(() => !!source()); + return { value, setValue, dispose: d }; + }); + flush(); expect(value()).toBe(false); setSource(1); + flush(); expect(value()).toBe(true); setValue(false); + flush(); expect(value()).toBe(false); setSource(2); + flush(); expect(value()).toBe(true); dispose(); diff --git a/packages/memo/tsconfig.json b/packages/memo/tsconfig.json index 1eb81e9f8..dc1970e16 100644 --- a/packages/memo/tsconfig.json +++ b/packages/memo/tsconfig.json @@ -6,9 +6,6 @@ "rootDir": "src" }, "references": [ - { - "path": "../scheduled" - }, { "path": "../utils" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c80903488..32fef6c0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,8 +270,8 @@ importers: version: link:../utils devDependencies: solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/devices: devDependencies: @@ -586,22 +586,16 @@ importers: packages/memo: dependencies: - '@solid-primitives/scheduled': - specifier: workspace:^ - version: link:../scheduled '@solid-primitives/utils': specifier: workspace:^ version: link:../utils devDependencies: - '@solid-primitives/mouse': - specifier: workspace:^ - version: link:../mouse - '@solidjs/router': - specifier: ^0.8.4 - version: 0.8.4(solid-js@1.9.7) + '@solidjs/web': + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10(solid-js@2.0.0-beta.10) solid-js: - specifier: ^1.9.7 - version: 1.9.7 + specifier: 2.0.0-beta.10 + version: 2.0.0-beta.10 packages/mouse: dependencies: @@ -2927,11 +2921,6 @@ packages: peerDependencies: solid-js: '>=1.8.4' - '@solidjs/router@0.8.4': - resolution: {integrity: sha512-Gi/WVoVseGMKS1DBdT3pNAMgOzEOp6Q3dpgNd2mW9GUEnVocPmtyBjDvXwN6m7tjSGsqqfqJFXk7bm1hxabSRw==} - peerDependencies: - solid-js: ^1.5.3 - '@solidjs/signals@2.0.0-beta.10': resolution: {integrity: sha512-McdmbLNiSlz616zcykS8Rb1t9QTOTKdNAoaWd4/OjXEbcAUrPqRX1CWgR+caiWUk4qn0a+LesTTV4jZhFFPaSg==} @@ -9602,10 +9591,6 @@ snapshots: dependencies: solid-js: 2.0.0-beta.12 - '@solidjs/router@0.8.4(solid-js@1.9.7)': - dependencies: - solid-js: 1.9.7 - '@solidjs/signals@2.0.0-beta.10': {} '@solidjs/signals@2.0.0-beta.12': {}