diff --git a/.changeset/little-mails-obey.md b/.changeset/little-mails-obey.md new file mode 100644 index 000000000..ea9ad20b0 --- /dev/null +++ b/.changeset/little-mails-obey.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/memo": patch +--- + +Correct `createLatest` and `createLatestMany` triggering on `equals: false` sources diff --git a/packages/memo/src/index.ts b/packages/memo/src/index.ts index ae6973415..15370ad4c 100644 --- a/packages/memo/src/index.ts +++ b/packages/memo/src/index.ts @@ -20,7 +20,7 @@ import { } from "solid-js"; import { isServer } from "solid-js/web"; import { debounce, throttle } from "@solid-primitives/scheduled"; -import { noop, EffectOptions } from "@solid-primitives/utils"; +import { noop, EffectOptions, EQUALS_FALSE_OPTIONS } from "@solid-primitives/utils"; export type MemoOptionsWithValue = MemoOptions & { value?: T }; export type AsyncMemoCalculation = (prev: T | Init) => Promise | T; @@ -106,7 +106,13 @@ export function createLatest[]>( options?: MemoOptions>, ): Accessor> { let index = 0; - const memos = sources.map((source, i) => createMemo(() => ((index = i), source()))); + 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); } @@ -131,18 +137,25 @@ export function createLatestMany( sources: readonly Accessor[], options?: EffectOptions, ): Accessor { - const mappedSources = sources.map(source => { - const obj: { dirty: boolean; memo: Accessor } = { dirty: true, memo: null as any }; - obj.memo = createMemo(() => ((obj.dirty = true), source())); + 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, + ); + return obj; }); + return createLazyMemo( () => - mappedSources.reduce((acc: T[], data) => { + memos.reduce((acc: T[], memo) => { // always track all memos to force updates - const v = data.memo(); - if (data.dirty) { - data.dirty = false; + const v = memo.get(); + if (memo.dirty) { + memo.dirty = false; acc.push(v); } return acc; @@ -348,8 +361,6 @@ export function createAsyncMemo( return state; } -const EQUALS_FALSE = { equals: false } as const; - /** * Lazily evaluated `createMemo`. Will run the calculation only if is being listened to. * @@ -394,11 +405,11 @@ export function createLazyMemo( let isReading = false, isStale: boolean | undefined = true; - const [track, trigger] = createSignal(void 0, EQUALS_FALSE), + 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, + DEV ? { name: options?.name, equals: false } : EQUALS_FALSE_OPTIONS, ); return (): T => { @@ -410,58 +421,6 @@ export function createLazyMemo( }; } -/* - -createCachedDerivation is a cool idea, but it has a n edgecase where it the value may get out of sync if read in a pure computation after it's sources. -And probably more then that, considering that the calculation is executed where read. - -*/ - -// /** -// * **! The value may get out of sync if read in a pure computation after it's sources !** -// * @param deps -// * @param fn -// * @param options -// * @returns -// */ -// export function createCachedDerivation( -// deps: AccessorArray | Accessor, -// fn: OnEffectFunction, Next>, -// options?: EffectOptions -// ): Accessor { -// let prevInput: S | undefined; -// let prev: undefined | NoInfer; -// let stale = true; - -// const track = createPureReaction(() => (stale = true), options); - -// const trackDeps = Array.isArray(deps) -// ? () => { -// for (const fn of deps) fn(); -// } -// : deps; - -// const getInput = Array.isArray(deps) -// ? () => { -// const res = Array(deps.length); -// for (let i = 0; i < deps.length; i++) res[i] = deps[i](); -// return res as S; -// } -// : deps; - -// return () => { -// if (stale) { -// let input!: S; -// track(() => (input = getInput())); -// prev = untrack(() => fn(input, prevInput, prev)); -// prevInput = input; -// stale = false; -// } -// trackDeps(); -// return prev as Next; -// }; -// } - export type CacheCalculation = (key: Key, prev: Value | undefined) => Value; export type CacheKeyAccessor = (key: Key) => Value; export type CacheOptions = MemoOptions & { size?: number }; diff --git a/packages/memo/test/latest.test.ts b/packages/memo/test/latest.test.ts index 9db1b8534..ff5eda811 100644 --- a/packages/memo/test/latest.test.ts +++ b/packages/memo/test/latest.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest"; -import { batch, createRoot, createSignal } from "solid-js"; +import { batch, createEffect, createRoot, createSignal } from "solid-js"; import { createLatest, createLatestMany } from "../src/index.js"; describe("createLatest", () => { @@ -27,6 +27,66 @@ describe("createLatest", () => { dispose(); }); }); + + test("works with equals: false sources", () => { + const [a, setA] = createSignal(0, { equals: false }); + const [b, setB] = createSignal("b"); + let captured: any; + + const dispose = createRoot(dispose => { + const latest = createLatest([a, b]); + createEffect(() => { + captured = latest(); + }); + return dispose; + }); + + expect(captured).toBe("b"); + captured = undefined; + + setA(1); + expect(captured).toBe(1); + captured = undefined; + + setB("c"); + expect(captured).toBe("c"); + captured = undefined; + + setA(1); + expect(captured).toBe(1); + + dispose(); + }); + + test("equals options", () => { + 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, + }; + }); + + expect(latest()).toBe("b"); + + setA(1); + expect(latest()).toBe("b"); + + setB("c"); + expect(latest()).toBe("c"); + + setA(2); + expect(latest()).toBe("c"); + + setB("d"); + expect(latest()).toBe("d"); + + dispose(); + }); }); describe("createLatestMany", () => { @@ -54,4 +114,34 @@ describe("createLatestMany", () => { dispose(); }); }); + + test("works with equals: false sources", () => { + const [a, setA] = createSignal(0, { equals: false }); + const [b, setB] = createSignal("b"); + let captured: any; + + const dispose = createRoot(dispose => { + const latest = createLatestMany([a, b]); + createEffect(() => { + captured = latest(); + }); + return dispose; + }); + + expect(captured).toEqual([0, "b"]); + captured = undefined; + + setA(1); + expect(captured).toEqual([1]); + captured = undefined; + + setB("c"); + expect(captured).toEqual(["c"]); + captured = undefined; + + setA(1); + expect(captured).toEqual([1]); + + dispose(); + }); });