Skip to content

Commit

Permalink
feat: isObservable
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Jun 27, 2022
1 parent 323f20e commit 88024da
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 39 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ $: yarn add @maverick-js/observables
- [`$readonly`](#readonly)
- [`$tick`](#tick)
- [`$dispose`](#dispose)
- [`isObservable`](#isobservable)
- [`isComputed`](#iscomputed)

## `$root`
Expand Down Expand Up @@ -326,6 +327,21 @@ $dispose($c, true); // <- deep flag
// `$a`, `$b`, and `$c` are all disposed.
```

### `isObservable`

Whether the given function is an observable.

```js
import { $observable, $computed, $effect, isObservable } from '@maverick-js/observables';

const $a = $observable(10);
isObservable($a); // true

isObservable(() => {}); // false
isObservable($computed(() => 10)); // false
isObservable($effect(() => {})); // false
```

### `isComputed`

Whether the given function is a computed observable.
Expand Down
22 changes: 22 additions & 0 deletions src/observables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type Computation<T> = {
export type Dispose = () => void;
export type StopEffect = (deep?: boolean) => void;

const OBSERVABLE = Symbol(__DEV__ ? 'OBSERVABLE' : '');
const COMPUTED = Symbol(__DEV__ ? 'COMPUTED' : '');
const DIRTY = Symbol(__DEV__ ? 'DIRTY' : '');
const DISPOSED = Symbol(__DEV__ ? 'DISPOSED' : '');
Expand Down Expand Up @@ -123,9 +124,29 @@ export function $observable<T>(initialValue: T, $id?: string): Observable<T> {

if (__DEV__) $observable.$id = $id ?? '$observable';

$observable[OBSERVABLE] = true;
return $observable;
}

/**
* Whether the given function is an observable.
*
* @example
* ```js
* const $a = $observable(10);
* isObservable($a); // true
*
* const $b = $computed(() => 10);
* isObservable($b); // false
*
* const $c = $effect(() => {});
* isObservable($c); // false
* ```
*/
export function isObservable<T>(fn: () => T): fn is Observable<T> {
return OBSERVABLE in fn;
}

/**
* Creates a new observable whose value is computed and returned by the given function. The given
* compute function is _only_ re-run when one of it's dependencies are updated. Dependencies are
Expand Down Expand Up @@ -323,6 +344,7 @@ export function safeNotEqual(a, b) {
type Computable = {
$id?: string;
(): any;
[OBSERVABLE]?: boolean;
[COMPUTED]?: boolean;
[DIRTY]?: boolean;
[DISPOSED]?: boolean;
Expand Down
91 changes: 52 additions & 39 deletions tests/observables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
$root,
type Computation,
type Observable,
isObservable,
} from '../src';

afterEach(() => $tick());
Expand All @@ -33,15 +34,15 @@ describe('$root', () => {
dispose();
});

expect($b!()).toEqual(20);
expect($b!()).toBe(20);
expect(computeB).toHaveBeenCalledTimes(1);

await $tick();

$a!.set(50);
await $tick();

expect($b!()).toEqual(20);
expect($b!()).toBe(20);
expect(computeB).toHaveBeenCalledTimes(1);
});

Expand All @@ -51,27 +52,39 @@ describe('$root', () => {
return 10;
});

expect(result).toEqual(10);
expect(result).toBe(10);
});
});

describe('$observable', () => {
it('should store and return value on read', () => {
const $a = $observable(10);
expect($a).toBeInstanceOf(Function);
expect($a()).toEqual(10);
expect($a()).toBe(10);
});

it('should update observable via `set()`', () => {
const $a = $observable(10);
$a.set(20);
expect($a()).toEqual(20);
expect($a()).toBe(20);
});

it('should update observable via `next()`', () => {
const $a = $observable(10);
$a.next((n) => n + 10);
expect($a()).toEqual(20);
expect($a()).toBe(20);
});
});

describe('isObservable', () => {
it('should return true if given observable', () => {
expect(isObservable($observable(10))).toBe(true);
});

it('should return false if given non-observable', () => {
expect(isObservable(() => {})).toBe(false);
expect(isObservable($computed(() => 10))).toBe(false);
expect(isObservable($effect(() => {}))).toBe(false);
});
});

Expand All @@ -81,11 +94,11 @@ describe('$computed', () => {
const $b = $observable(10);
const $c = $computed(() => $a() + $b());

expect($c()).toEqual(20);
expect($c()).toBe(20);
await $tick();

// Try again to ensure state is maintained.
expect($c()).toEqual(20);
expect($c()).toBe(20);
});

it('should update when dependency is updated', () => {
Expand All @@ -94,10 +107,10 @@ describe('$computed', () => {
const $c = $computed(() => $a() + $b());

$a.set(20);
expect($c()).toEqual(30);
expect($c()).toBe(30);

$b.set(20);
expect($c()).toEqual(40);
expect($c()).toBe(40);
});

it('should update when deep dependency is updated', async () => {
Expand All @@ -107,7 +120,7 @@ describe('$computed', () => {
const $d = $computed(() => $c());

$a.set(20);
expect($d()).toEqual(30);
expect($d()).toBe(30);
});

it('should update when deep computed dependency is updated', () => {
Expand All @@ -118,7 +131,7 @@ describe('$computed', () => {
const $e = $computed(() => $d());

$a.set(20);
expect($e()).toEqual(30);
expect($e()).toBe(30);
});

it('should only re-compute when needed', () => {
Expand Down Expand Up @@ -173,23 +186,23 @@ describe('$computed', () => {
$e();
expect(computeC).toHaveBeenCalledTimes(1);
expect(computeD).toHaveBeenCalledTimes(1);
expect($e()).toEqual(20);
expect($e()).toBe(20);

$a.set(20);
await $tick();

$e();
expect(computeC).toHaveBeenCalledTimes(2);
expect(computeD).toHaveBeenCalledTimes(1);
expect($e()).toEqual(30);
expect($e()).toBe(30);

$b.set(20);
await $tick();

$e();
expect(computeC).toHaveBeenCalledTimes(2);
expect(computeD).toHaveBeenCalledTimes(2);
expect($e()).toEqual(40);
expect($e()).toBe(40);
});
});

Expand Down Expand Up @@ -257,7 +270,7 @@ describe('$effect', () => {
await $tick();

expect(effect).toHaveBeenCalledTimes(1);
expect($b()).toEqual(10);
expect($b()).toBe(10);
});
});

Expand All @@ -275,9 +288,9 @@ describe('$peek', () => {

$effect(() => {
effect();
expect($peek($a)).toEqual(10);
expect($peek($b)).toEqual(20);
expect($peek($c)).toEqual(30);
expect($peek($a)).toBe(10);
expect($peek($b)).toBe(20);
expect($peek($c)).toBe(30);
});

expect(effect).toHaveBeenCalledTimes(1);
Expand All @@ -303,31 +316,31 @@ describe('$peek', () => {

$effect(() => {
effect();
expect($peek($a)).toEqual(10);
expect($peek($d)).toEqual(40);
expect($peek($a)).toBe(10);
expect($peek($d)).toBe(40);
});

expect(effect).toHaveBeenCalledTimes(1);
expect(computeD).toHaveBeenCalledTimes(1);
expect($d()).toEqual(40);
expect($d()).toBe(40);

$a.set(20);
await $tick();
expect(effect).toHaveBeenCalledTimes(1);
expect(computeD).toHaveBeenCalledTimes(2);
expect($d()).toEqual(50);
expect($d()).toBe(50);

$b.set(20);
await $tick();
expect(effect).toHaveBeenCalledTimes(1);
expect(computeD).toHaveBeenCalledTimes(2);
expect($d()).toEqual(50);
expect($d()).toBe(50);

$c.set(20);
await $tick();
expect(effect).toHaveBeenCalledTimes(1);
expect(computeD).toHaveBeenCalledTimes(2);
expect($d()).toEqual(50);
expect($d()).toBe(50);
});
});

Expand Down Expand Up @@ -389,11 +402,11 @@ describe('$readonly', () => {
}).toThrow();

await $tick();
expect($b()).toEqual(10);
expect($b()).toBe(10);

$a.set(20);
await $tick();
expect($b()).toEqual(20);
expect($b()).toBe(20);
});
});

Expand All @@ -405,20 +418,20 @@ describe('$dispose', () => {
const $d = $computed(() => $c() + 10);
const $e = $computed(() => $a() + $b() + $d());

expect($e()).toEqual(50);
expect($e()).toBe(50);

$dispose($a);

$a.set(20);
await $tick();

expect($b()).toEqual(20);
expect($e()).toEqual(50);
expect($b()).toBe(20);
expect($e()).toBe(50);

// $c/$d should keep working.
$c.set(20);
await $tick();
expect($d()).toEqual(30);
expect($d()).toBe(30);
});

it('should dispose (deep)', async () => {
Expand All @@ -436,10 +449,10 @@ describe('$dispose', () => {
$a.set(20);
await $tick();

expect($a()).toEqual(10);
expect($c()).toEqual(40);
expect($d()).toEqual(60);
expect($e()).toEqual(110);
expect($a()).toBe(10);
expect($c()).toBe(40);
expect($d()).toBe(60);
expect($e()).toBe(110);

$_b.set(100);
expect($b()).to.equal(20);
Expand All @@ -448,18 +461,18 @@ describe('$dispose', () => {

describe('isComputed', () => {
it('should return false given function', () => {
expect(isComputed(() => {})).toEqual(false);
expect(isComputed(() => {})).toBe(false);
});

it('should return false given observable', () => {
expect(isComputed($observable(10))).toEqual(false);
expect(isComputed($observable(10))).toBe(false);
});

it('should return false given effect', () => {
expect(isComputed($effect(() => {}))).toEqual(false);
expect(isComputed($effect(() => {}))).toBe(false);
});

it('should return true given computed', () => {
expect(isComputed($computed(() => {}))).toEqual(true);
expect(isComputed($computed(() => {}))).toBe(true);
});
});

0 comments on commit 88024da

Please sign in to comment.