context
Version:
Lightweight context propagation for JavaScript and TypeScript. Create a scoped storage object, run code inside it, and read the active value anywhere down the call stack - without depending on React.
101 lines (80 loc) • 2.42 kB
text/typescript
import type { CB, Maybe } from 'vest-utils';
import {
assign,
defaultTo,
invariant,
dynamicValue,
Nullable,
} from 'vest-utils';
const USEX_DEFAULT_ERROR_MESSAGE = 'Not inside of a running context.';
const EMPTY_CONTEXT = Symbol();
/**
* Base context interface.
*/
export function createContext<T>(defaultContextValue?: T): CtxApi<T> {
let contextValue: T | symbol = EMPTY_CONTEXT;
return {
run,
use,
useX,
};
function use(): T {
return (isInsideContext() ? contextValue : defaultContextValue) as T;
}
function useX(errorMessage?: string): T {
invariant(
isInsideContext(),
defaultTo(errorMessage, USEX_DEFAULT_ERROR_MESSAGE),
);
return contextValue as T;
}
function run<R>(value: T, cb: () => R): R {
const parentContext = isInsideContext() ? use() : EMPTY_CONTEXT;
contextValue = value;
const res = cb();
contextValue = parentContext;
return res;
}
function isInsideContext(): boolean {
return contextValue !== EMPTY_CONTEXT;
}
}
/**
* Cascading context - another implementation of context, that assumes the context value is an object.
* When nesting context runs, the the values of the current layer merges with the layers above it.
*/
export function createCascade<T extends Record<string, unknown>>(
init?: (value: Partial<T>, parentContext: Maybe<T>) => Nullable<T>,
): CtxCascadeApi<T> {
const ctx = createContext<T>();
return {
bind,
run,
use: ctx.use,
useX: ctx.useX,
};
function run<R>(value: Partial<T>, fn: () => R): R {
const parentContext = ctx.use();
const initResult = dynamicValue(init, value, parentContext) ?? value;
const out = assign({}, parentContext ? parentContext : {}, initResult) as T;
return ctx.run(Object.freeze(out), fn) as R;
}
function bind<Fn extends CB>(value: Partial<T>, fn: Fn) {
return function (...runTimeArgs: Parameters<Fn>) {
return run<ReturnType<Fn>>(value, function () {
return fn(...runTimeArgs);
});
} as Fn;
}
}
type ContextConsumptionApi<T> = {
use: () => T;
useX: (errorMessage?: string) => T;
};
export type CtxApi<T> = ContextConsumptionApi<T> & {
run: <R>(value: T, cb: () => R) => R;
};
export type CtxCascadeApi<T> = ContextConsumptionApi<T> & {
run: <R>(value: Partial<T>, fn: () => R) => R;
bind: <Fn extends CB>(value: Partial<T>, fn: Fn) => Fn;
};