diff --git a/README.md b/README.md index 421c6f7d..4d4f47c1 100644 --- a/README.md +++ b/README.md @@ -46,12 +46,12 @@ npm add value-enhancer | import | size(brotli) | | ----------------------------- | ------------ | -| `*` | 1.81 kB | +| `*` | 1.85 kB | | `{ readonlyVal, val }` (core) | 1.05 kB | | `{ from }` | 26 B | | `{ derive }` | 93 B | | `{ combine }` | 204 B | -| `{ compute }` | 213 B | +| `{ compute }` | 270 B | | `{ flattenFrom }` | 227 B | | `{ flatten }` | 36 B | | `{ reactiveMap }` | 489 B | diff --git a/src/compute.ts b/src/compute.ts index 002bcd02..4b6b4dfc 100644 --- a/src/compute.ts +++ b/src/compute.ts @@ -1,6 +1,6 @@ import { ValAgent } from "./agent"; -import type { ReadonlyVal, UnwrapVal, ValConfig, ValDisposer } from "./typings"; -import { invoke, isVal } from "./utils"; +import type { ReadonlyVal, UnwrapVal, ValConfig, ValVersion } from "./typings"; +import { INIT_VALUE, isVal, strictEqual } from "./utils"; import { ValImpl } from "./val"; export interface ComputeGet { @@ -39,8 +39,8 @@ export const compute = ( ): ReadonlyVal => { let scopeLevel = 0; - let currentDisposers = new Map(); - let oldDisposers = new Map(); + let currentDeps = new Map(); + let oldDeps = new Map(); const get = ( val$?: T | ReadonlyVal | { $: ReadonlyVal } @@ -52,40 +52,61 @@ export const compute = ( val$ = (val$ as { $: ReadonlyVal }).$; } - if (!currentDisposers.has(val$)) { - let disposer = oldDisposers.get(val$); - if (disposer) { - oldDisposers.delete(val$); - } else { - disposer = val$.$valCompute(agent.notify_); + if (currentDeps.has(val$)) { + if (!strictEqual(val$.$version, currentDeps.get(val$))) { + // depends on multiple versions of the same val + // so we set a unique version here + currentDeps.set(val$, INIT_VALUE); } - currentDisposers.set(val$, disposer); + } else { + if (!oldDeps.delete(val$)) { + val$.$valCompute(agent.notify_); + } + currentDeps.set(val$, val$.$version); } + return val$.value; }; + let currentValue: TValue; const agent = new ValAgent( () => { - if (!scopeLevel++) { - const tmp = currentDisposers; - currentDisposers = oldDisposers; - oldDisposers = tmp; + if (!scopeLevel) { + if (currentDeps.size) { + x: { + for (const [dep, version] of currentDeps) { + if (!strictEqual(dep.$version, version)) { + break x; + } + } + return currentValue; + } + } + + const tmp = currentDeps; + currentDeps = oldDeps; + oldDeps = tmp; + scopeLevel++; } - const value = effect(get); + currentValue = effect(get); - if (!--scopeLevel && oldDisposers.size) { - oldDisposers.forEach(invoke); - oldDisposers.clear(); + if (!--scopeLevel && oldDeps.size) { + for (const dep of oldDeps.keys()) { + dep.unsubscribe(agent.notify_); + } + oldDeps.clear(); } - return value; + return currentValue; }, config, () => () => { - if (currentDisposers.size) { - currentDisposers.forEach(invoke); - currentDisposers.clear(); + if (currentDeps.size) { + for (const dep of currentDeps.keys()) { + dep.unsubscribe(agent.notify_); + } + currentDeps.clear(); } } ); diff --git a/test/compute.test.ts b/test/compute.test.ts index 727c2028..e2896f53 100644 --- a/test/compute.test.ts +++ b/test/compute.test.ts @@ -100,4 +100,36 @@ describe("compute", () => { s$.dispose(); }); + + it("should check deps before compute", async () => { + const a = val(0); + + const spyB = jest.fn(); + const b = compute(get => { + spyB(); + return get(a) > 10; + }); + + const spyC = jest.fn(); + const c = compute(get => { + spyC(); + return get(b) + ""; + }); + + const spySub = jest.fn(); + c.reaction(spySub, true); + + expect(spyB).toBeCalledTimes(1); + expect(spyC).toBeCalledTimes(1); + expect(spySub).toBeCalledTimes(0); + + spyB.mockClear(); + spyC.mockClear(); + + a.set(2); + + expect(spyB).toBeCalledTimes(1); + expect(spyC).toBeCalledTimes(0); + expect(spySub).toBeCalledTimes(0); + }); });