diff --git a/README.md b/README.md index aeb79c7..ba4983a 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ $: yarn add @maverick-js/observables - [`isObservable`](#isobservable) - [`isSubject`](#issubject) - [`getParent`](#getparent) +- [`getContext`](#getcontext) +- [`setContext`](#setcontext) - [`getScheduler`](#getscheduler) ## `root` @@ -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 diff --git a/src/observables.ts b/src/observables.ts index 031564e..cda3b9d 100644 --- a/src/observables.ts +++ b/src/observables.ts @@ -22,16 +22,18 @@ export type MaybeDispose = Maybe; export type MaybeStopEffect = Maybe; export type MaybeObservable = MaybeFunction | Observable; -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; + +const PARENT = Symbol(), + OBSERVABLE = Symbol(), + COMPUTED = Symbol(), + DIRTY = Symbol(), + DISPOSED = Symbol(), + OBSERVERS = Symbol(), + CHILDREN = Symbol(), + DISPOSAL = Symbol(), + CONTEXT = Symbol(), + NOOP = () => {}; const _scheduler = createScheduler(); @@ -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; } @@ -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. @@ -507,6 +541,7 @@ type Node = { [DISPOSED]?: boolean; [OBSERVERS]?: Set; [CHILDREN]?: Set; + [CONTEXT]?: ContextRecord; [DISPOSAL]?: Set; }; @@ -527,6 +562,17 @@ function compute(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; diff --git a/tests/context.test.ts b/tests/context.test.ts new file mode 100644 index 0000000..883b6c5 --- /dev/null +++ b/tests/context.test.ts @@ -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); + }); + }); + }); +});