Skip to content

Commit

Permalink
feat: getContext and setContext
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Aug 9, 2022
1 parent 61b896b commit 9578cd0
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 11 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ $: yarn add @maverick-js/observables
- [`isObservable`](#isobservable)
- [`isSubject`](#issubject)
- [`getParent`](#getparent)
- [`getContext`](#getcontext)
- [`setContext`](#setcontext)
- [`getScheduler`](#getscheduler)

## `root`
Expand Down Expand Up @@ -489,6 +491,44 @@ root(() => {
});
```

### `getContext`

Attempts to get a context value for the given key. It will start from the parent scope and
walk up the computation tree trying to find a context record and matching key. If no value can be
found `undefined` will be returned. This is intentionally low-level so you can design a context API
in your library as desired.

In your implementation make sure to check if a parent exists via `getParent()`. If one does
not exist log a warning that this function should not be called outside a computation or render
function.

> **Note**
> See the `setContext` code example below for a demo of this function.
### `setContext`

Attempts to set a context value on the parent scope with the given key. This will be a no-op if
no parent is defined. This is intentionally low-level so you can design a context API in your
library as desired.

In your implementation make sure to check if a parent exists via `getParent()`. If one does
not exist log a warning that this function should not be called outside a computation or render
function.

```js
import { root, getContext, setContext } from '@maverick-js/observables';

const key = Symbol();

root(() => {
setContext(key, 100);
// ...
root(() => {
const value = getContext(key); // 100
});
});
```

### `getScheduler`

Returns the global scheduler which can be used to queue additional tasks or synchronously flush
Expand Down
68 changes: 57 additions & 11 deletions src/observables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,18 @@ export type MaybeDispose = Maybe<Dispose>;
export type MaybeStopEffect = Maybe<StopEffect>;
export type MaybeObservable<T> = MaybeFunction | Observable<T>;

const NOOP = () => {};

const PARENT = Symbol();
const OBSERVABLE = Symbol();
const COMPUTED = Symbol();
const DIRTY = Symbol();
const DISPOSED = Symbol();
const OBSERVERS = Symbol();
const CHILDREN = Symbol();
const DISPOSAL = Symbol();
export type ContextRecord = Record<string | symbol, unknown>;

const PARENT = Symbol(),
OBSERVABLE = Symbol(),
COMPUTED = Symbol(),
DIRTY = Symbol(),
DISPOSED = Symbol(),
OBSERVERS = Symbol(),
CHILDREN = Symbol(),
DISPOSAL = Symbol(),
CONTEXT = Symbol(),
NOOP = () => {};

const _scheduler = createScheduler();

Expand Down Expand Up @@ -215,10 +217,11 @@ export function dispose(fn: () => void) {

emptyDisposal(fn);

fn[PARENT] = undefined;
fn[CHILDREN] = undefined;
fn[DISPOSAL] = undefined;
fn[OBSERVERS] = undefined;
fn[PARENT] = undefined;
fn[CONTEXT] = undefined;
fn[DIRTY] = false;
fn[DISPOSED] = true;
}
Expand Down Expand Up @@ -290,6 +293,37 @@ export function getScheduler(): Scheduler {
return _scheduler;
}

/**
* Attempts to get a context value for the given key. It will start from the parent scope and
* walk up the computation tree trying to find a context record and matching key. If no value can
* be found `undefined` will be returned. This is intentionally low-level so you can design a
* context API in your library as desired.
*
* In your implementation make sure to check if a parent exists via `getParent()`. If one does
* not exist log a warning that this function should not be called outside a computation or render
* function.
*
* @see {@link https://github.com/maverick-js/observables#getcontext}
*/
export function getContext(key: string | symbol): unknown {
return lookup(_parent, key);
}

/**
* Attempts to set a context value on the parent scope with the given key. This will be a no-op if
* no parent is defined. This is intentionally low-level so you can design a context API in your
* library as desired.
*
* In your implementation make sure to check if a parent exists via `getParent()`. If one does
* not exist log a warning that this function should not be called outside a computation or render
* function.
*
* @see {@link https://github.com/maverick-js/observables#setcontext}
*/
export function setContext(key: string | symbol, value: unknown) {
if (_parent) (_parent[CONTEXT] ??= {})[key] = value;
}

// Adapted from: https://github.com/solidjs/solid/blob/main/packages/solid/src/reactive/array.ts#L153
/**
* Reactive map helper that caches each item by index to reduce unnecessary mapping on updates.
Expand Down Expand Up @@ -507,6 +541,7 @@ type Node = {
[DISPOSED]?: boolean;
[OBSERVERS]?: Set<Node>;
[CHILDREN]?: Set<Node>;
[CONTEXT]?: ContextRecord;
[DISPOSAL]?: Set<Dispose>;
};

Expand All @@ -527,6 +562,17 @@ function compute<T>(parent: () => void, child: () => T): T {
return nextValue;
}

function lookup(fn: Node | undefined, key: string | symbol): any {
let current = fn,
value;

while (current) {
value = current[CONTEXT]?.[key];
if (value !== undefined) return value;
current = current[PARENT];
}
}

function adoptChild(child: Node) {
if (_parent) {
child[PARENT] = _parent;
Expand Down
24 changes: 24 additions & 0 deletions tests/context.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { effect, getContext, root, setContext } from '../src';

it('should get context value', () => {
const key = Symbol();
root(() => {
setContext(key, 100);
root(() => {
effect(() => {
expect(getContext(key)).toBe(100);
});
});
});
});

it('should not throw if no context value is found', () => {
const key = Symbol();
root(() => {
root(() => {
effect(() => {
expect(getContext(key)).toBe(undefined);
});
});
});
});

0 comments on commit 9578cd0

Please sign in to comment.