Skip to content

Commit

Permalink
fix: auto-dispose computed if no observers
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Nov 25, 2022
1 parent c2daca8 commit 0ab5855
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 45 deletions.
89 changes: 44 additions & 45 deletions src/observables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ export function computed<T>(
currentValue = nextValue;
dirtyNode($computed);
}

if (!$computed[OBSERVING]?.size) dispose($computed);
} catch (error) {
handleError($computed, error);
}
Expand All @@ -216,49 +218,6 @@ export function isObserved(): boolean {
return !!currentObserver?.[OBSERVING]?.size;
}

/**
* Runs the given function when the parent scope computation is being disposed.
*
* @see {@link https://github.com/maverick-js/observables#ondispose}
*/
export function onDispose(dispose: MaybeDispose): Dispose {
if (!dispose || !currentScope || currentScope[DISPOSED]) return NOOP;
(currentScope[DISPOSAL] ??= new Set()).add(dispose);
return () => {
(dispose as Dispose)();
currentScope![DISPOSAL]?.delete(dispose as Dispose);
};
}

/**
* Unsubscribes the given observable and all inner computations. Disposed functions will retain
* their current value but are no longer reactive.
*
* @see {@link https://github.com/maverick-js/observables#dispose}
*/
export function dispose(fn: () => void) {
if (fn[DISPOSED]) return;

const children = fn[CHILDREN];
if (children) for (const child of children) dispose(child);

const observing = fn[OBSERVING];
if (observing) for (const node of observing) node[OBSERVED_BY]?.delete(fn);

emptyDisposal(fn);
fn[SCOPE] = undefined;
fn[OBSERVING] = undefined;
fn[CHILDREN]?.clear();
fn[CHILDREN] = undefined;
fn[DISPOSAL]?.clear();
fn[DISPOSAL] = undefined;
fn[OBSERVED_BY]?.clear();
fn[OBSERVED_BY] = undefined;
fn[CONTEXT] = undefined;
fn[DIRTY] = false;
fn[DISPOSED] = true;
}

/**
* Invokes the given function each time any of the observables that are read inside are updated
* (i.e., their value changes). The effect is immediately invoked on initialization.
Expand Down Expand Up @@ -381,6 +340,46 @@ export function onError<T = Error>(handler: (error: T) => void): void {
(((currentScope[CONTEXT] ??= {})[ERROR] as Set<any>) ??= new Set()).add(handler);
}

/**
* Runs the given function when the parent scope computation is being disposed.
*
* @see {@link https://github.com/maverick-js/observables#ondispose}
*/
export function onDispose(dispose: MaybeDispose): Dispose {
if (!dispose || !currentScope || currentScope[DISPOSED]) return NOOP;
(currentScope[DISPOSAL] ??= new Set()).add(dispose);
return () => {
(dispose as Dispose)();
currentScope![DISPOSAL]?.delete(dispose as Dispose);
};
}

/**
* Unsubscribes the given observable and all inner computations. Disposed functions will retain
* their current value but are no longer reactive.
*
* @see {@link https://github.com/maverick-js/observables#dispose}
*/
export function dispose(fn: () => void) {
if (fn[DISPOSED]) return;

if (fn[CHILDREN]) for (const node of fn[CHILDREN]) dispose(node);
if (fn[OBSERVING]) for (const node of fn[OBSERVING]) node[OBSERVED_BY]?.delete(fn);
if (fn[OBSERVED_BY]) for (const node of fn[OBSERVED_BY]) node[OBSERVING]?.delete(fn);

emptyDisposal(fn);
fn[SCOPE] = undefined;
fn[CHILDREN]?.clear();
fn[CHILDREN] = undefined;
fn[OBSERVING]?.clear();
fn[OBSERVING] = undefined;
fn[OBSERVED_BY]?.clear();
fn[OBSERVED_BY] = undefined;
fn[DISPOSAL] = undefined;
fn[CONTEXT] = undefined;
fn[DISPOSED] = true;
}

function compute<T>(scope: () => void, node: () => T, observer: () => void = scope): T {
const prevScope = currentScope;
const prevObserver = currentObserver;
Expand Down Expand Up @@ -410,13 +409,13 @@ function lookup(node: Node | undefined, key: string | symbol): any {
}

function adopt(node: Node, scope = currentScope) {
if (!scope || scope[DISPOSED]) return;
if (!scope) return;
node[SCOPE] = scope;
(scope[CHILDREN] ??= new Set()).add(node);
}

function observe(observable: Node, observer: Node) {
if (observable[DISPOSED]) return;
if (observable[DISPOSED] || observer[DISPOSED]) return;
(observable[OBSERVED_BY] ??= new Set()).add(observer);
(observer[OBSERVING] ??= new Set()).add(observable);
}
Expand Down
12 changes: 12 additions & 0 deletions tests/computed.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { computed, effect, observable, tick } from '../src';
import { DISPOSED } from '../src/symbols';

afterEach(() => tick());

Expand Down Expand Up @@ -177,3 +178,14 @@ it('should accept dirty option', async () => {
expect($b()).toBe(2);
expect(effectA).toHaveBeenCalledTimes(2);
});

it('should auto-dispose computed if not observing anything', () => {
const $a = computed(() => null, { id: '$a' });
$a();

const $b = computed(() => $a(), { id: '$b' });
$b();

expect($a[DISPOSED]).toBeTruthy();
expect($b[DISPOSED]).toBeTruthy();
});
2 changes: 2 additions & 0 deletions tests/peek.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ it('should not trigger deep `onDispose`', async () => {
const dispose = vi.fn();
const computeB = vi.fn();

const $a = observable(0);
const $b = computed(() => {
$a();
computeB();
onDispose(dispose);
return 10;
Expand Down

0 comments on commit 0ab5855

Please sign in to comment.