UNPKG

rvx

Version:

A signal based rendering library

200 lines (183 loc) 5.46 kB
import { CONTEXT_WINDOWS } from "./internals/stacks.js"; /** * Internal function to capture the current state of the specified context. */ const _capture = <T>(context: Context<T>): ContextState<T> => { return { context: context, value: context.current, }; }; /** * A context for implicitly passing values along the call stack. * * If you need a global default value, use {@link DefaultContext} instead. */ export class Context<T> { /** * @param defaultValue The default value. This is used if the {@link current} value is `null` or `undefined`. */ constructor(defaultValue: T); constructor(...defaultValue: T extends undefined ? [] : [T]); constructor(defaultValue?: T) { this.default = defaultValue!; } /** * The stack of provided values. */ #stack: (T | null | undefined)[] = []; /** * The innermost context window id (index in the context window stack) in which a value has been provided. * * Provided values are ignored if this mismatches the current context window. */ #windowId = 0; /** * Get or set the default value. * * This is used if the {@link current} value is `null` or `undefined`. */ default: T; /** * Get the current value for this context. */ get current(): T { if (this.#windowId === CONTEXT_WINDOWS.length) { const stack = this.#stack; return stack[stack.length - 1] ?? this.default; } return this.default; } /** * Run a function while injecting the specified value for this context. * * See {@link Inject `<Inject>`} when using JSX. * * @param value The value to inject. * @param fn The function to run. * @param args The function arguments. * @returns The function's return value. */ inject<F extends (...args: any) => any>(value: T | null | undefined, fn: F, ...args: Parameters<F>): ReturnType<F> { const window = CONTEXT_WINDOWS[CONTEXT_WINDOWS.length - 1]; const stack = this.#stack; const parent = this.#windowId; try { this.#windowId = CONTEXT_WINDOWS.length; window.push(this); stack.push(value); return fn(...args); } finally { stack.pop(); window.pop(); this.#windowId = parent; } } /** * Shorthand for creating a context-value pair for this context. */ with(value: T | null | undefined): ContextState<T> { return { context: this, value }; } /** * Run a function in a new context window (ignoring all current contexts) while injecting the specified states. * * @param states The states to inject. * @param fn The function to run. * @param args The function arguments. * @returns The function's return value. */ static window<F extends (...args: any) => any>(states: ContextState<unknown>[], fn: F, ...args: Parameters<F>): ReturnType<F> { try { CONTEXT_WINDOWS.push([]); return Context.inject<F>(states, fn, ...args); } finally { CONTEXT_WINDOWS.pop(); } } /** * Run a function while injecting the specified states. * * See {@link Inject `<Inject>`} when using JSX. * * @param states The states to inject. When providing multiple values for the same context, the last one is used. * @param fn The function to run. * @param args The function arguments. * @returns The function's return value. */ static inject<F extends (...args: any) => any>(states: ContextState<unknown>[], fn: F, ...args: Parameters<F>): ReturnType<F> { const active: ActiveState<unknown>[] = []; const windowId = CONTEXT_WINDOWS.length; const window = CONTEXT_WINDOWS[windowId - 1]; for (let i = 0; i < states.length; i++) { const { context, value } = states[i]; active.push({ c: context, p: context.#windowId }); context.#windowId = windowId; context.#stack.push(value); window.push(context); } try { return fn(...args); } finally { for (let i = active.length - 1; i >= 0; i--) { const { c: context, p: parent } = active[i]; context.#windowId = parent; context.#stack.pop(); window.pop(); } } } /** * Capture all current context states. */ static capture(): ContextState<unknown>[] { return CONTEXT_WINDOWS[CONTEXT_WINDOWS.length - 1].map(_capture); } /** * Capture all current context states and wrap a function to always run with only these states injected. * * @param fn The function to wrap. * @returns The wrapped function. */ static wrap<T extends (...args: any) => any>(fn: T): T { const states = Context.capture(); return ((...args) => Context.window<any>(states, fn, ...args)) as T; } } interface ActiveState<T> { /** The active context. */ c: Context<T>; /** The context window id before this state has been activated. */ p: number; } /** * A context-value pair. */ export interface ContextState<T> { context: Context<T>; /** * The value that is injected when using this state with {@link Inject `<Inject>`}, {@link Context.inject} or {@link Context.window}. */ value: T | null | undefined; } /** * Component for injecting context values while rendering. * * See {@link Context.inject} when not using JSX. */ export function Inject<T>(props: { /** The context to inject into. */ context: Context<T>; /** The value to inject. */ value: T | null | undefined; children: () => unknown; } | { /** The context states to inject. */ states: ContextState<unknown>[]; children: () => unknown; }): unknown { if ("context" in props) { return props.context.inject(props.value, props.children); } return Context.inject(props.states, props.children); }