UNPKG

@robicue/use-context

Version:

The official useContext implementation for the Robicue Hook Architecture

404 lines (352 loc) 9.72 kB
/** * The context type */ export type Context = object; /** * The states holder type */ export type States = Map<Hook<any, unknown, any>, any>; /** * A simple hook type */ export type Hook<A extends unknown[], R, C extends Context = Context> = ( context: C, ...args: A ) => R; /** * An extendable hook type */ export type ExtendedHook< A extends unknown[], R, C extends Context = Context > = (originalHook: Hook<A, R, C>, context: C, ...args: A) => R; /** * Holds all the context states */ const contexts = new WeakMap<Context, States>(); /** * Holds all the links to parent contexts */ const parents = new WeakMap<Context, Context>(); /** * Holds all the extendable hook functions */ const extendables = new WeakMap< Function, Hook<[], ExtendedHook<any, any, any>, any> >(); /** * Get the states associated with the specified initializer */ const getStates = <A extends unknown[], R, C extends Context = Context>( context: Context, initializer: Hook<A, R, C> ): States | undefined => { const states = contexts.get(context); if (states?.has(initializer)) { return states; } const parent = parents.get(context); if (!parent) { return undefined; } return getStates(parent, initializer); }; /** * Checks if the provided value is a valid context object */ export const isContext = (value: Context) => { return contexts.has(value); }; /** * Checks if the specified context is a forked context */ export const isForked = (context: Context) => { return parents.has(context); }; /** * Returns TRUE if the specified initializer is used in the context */ export const isUsed = <A extends unknown[], R, C extends Context = Context>( context: C, initializer: Hook<A, R, C> ) => { return !!getStates(context, initializer); }; /** * Set a new state into the context */ export const set = <A extends unknown[], R, C extends Context = Context>( context: C, initializer: Hook<A, R, C>, state: R ) => { let states = contexts.get(context); if (!states) { states = new Map(); contexts.set(context, states); } states.set(initializer, state); return state; }; /** * Initializes a new state into the context */ export const init = <A extends unknown[], R, C extends Context = Context>( context: C, initializer: Hook<A, R, C>, ...args: A ) => { const state = initializer(context, ...args); return set(context, initializer, state); }; /** * Uses or initializes a state in the context */ export const use = <A extends unknown[], R, C extends Context = Context>( context: C, initializer: Hook<A, R, C>, ...args: A ) => { const states = getStates(context, initializer); if (!states) { return init(context, initializer, ...args); } return states.get(initializer) as R; }; /** * Calls the initializer or gets a state in the context */ export const call = <A extends unknown[], R, C extends Context = Context>( context: C, initializer: Hook<A, R, C>, ...args: A ) => { const states = getStates(context, initializer); if (!states) { return initializer(context, ...args); } return states.get(initializer) as R; }; /** * Gets a state that has already been initialized */ export const get = <A extends unknown[], R, C extends Context = Context>( context: C, initializer: Hook<A, R, C> ) => { const states = getStates(context, initializer); if (!states) { throw new Error("Not yet initialized"); } return states.get(initializer) as R; }; /** * Makes a fork of all forkable states the current context. * The forked context allows you to keep access to the states * of the current context, but newly initialized states, created * in the forked context, are not seen by hooks using the current context. */ export const fork = <C extends Context = Context>( context: Context, forkedContext: C = {} as C ): C => { if (context === forkedContext) { throw new Error( "The parent context and the forked context cannot be the same" ); } if (isContext(forkedContext)) { throw new Error("The forked context is already in use"); } if (!isContext(context)) { contexts.set(context, new Map()); } contexts.set(forkedContext, new Map()); parents.set(forkedContext, context); return forkedContext; }; /** * Checks if the state of an initializer is inherited form a parent context */ export const isInherited = < A extends unknown[], R, C extends Context = Context >( context: Context, initializer: Hook<A, R, C> ) => { const states = getStates(context, initializer); if (!states) { return false; } return states !== contexts.get(context); }; /** * Creates a hook function */ export const hook = <A extends unknown[], R, C extends Context = Context>( func: (context: C, ...args: A) => R ): ((context: C, ...args: A) => R) => { return func; }; /** * Creates a clone of a hook function. */ export const clone = <A extends unknown[], R, C extends Context = Context>( func: (context: C, ...args: A) => R ): ((context: C, ...args: A) => R) => { return (context, ...args) => { return func(context, ...args); }; }; /** * Creates an anchor hook factory */ export const factory = <A extends unknown[], R, C extends Context = Context>( func: (context: C, ...args: A) => R ): (() => (context: C, ...args: A) => R) => { return () => anchor(clone(func)); }; /** * Creates a hook that memorizes the result in the context */ export const anchor = <A extends unknown[], R, C extends Context = Context>( func: (context: C, ...args: A) => R ): ((context: C, ...args: A) => R) => { return (context, ...args) => { return use(context, func, ...args); }; }; /** * Creates a hook that can be extended */ export const extendable = <A extends unknown[], R, C extends Context = Context>( func: (context: C, ...args: A) => R ): ((context: C, ...args: A) => R) => { const extendableFunc: Hook<[], ExtendedHook<A, R, C>, C> = () => (originalHook, ...args) => originalHook(...args); const result: (context: C, ...args: A) => R = (context, ...args) => { return call(context, extendableFunc)(func, context, ...args); }; extendables.set(result, extendableFunc); return result; }; /** * Extends a hook within a certain context */ export const extend = <A extends unknown[], R, C extends Context = Context>( context: C, func: Hook<A, R, C>, extension: ExtendedHook<A, R, C> ) => { const extendableFunc = extendables.get(func) as Hook< [], ExtendedHook<A, R, C>, C >; if (!extendableFunc) { throw new Error("The hook does not support extensions"); } const previousHook = call(context, extendableFunc); set(context, extendableFunc, (originalHook, ...args) => { return extension((...a) => previousHook(originalHook, ...a), ...args); }); }; /** * Creates a utility hook that memorizes the result of the context. * The context is optional here. If not provided, the specified * function itself will be used as context. */ export const util = <A extends unknown[], R>( func: (context: Context, ...args: A) => R ): ((context?: Context, ...args: A) => R) => { return (context, ...args) => { return use(context ?? func, func, ...args); }; }; /** * Creates an hook that memorizes the result in the unforkable context */ export const buoy = <A extends unknown[], R, C extends Context = Context>( func: (context: C, ...args: A) => R ): ((context: C, ...args: A) => R) => { return (context, ...args) => { if (isInherited(context, func)) { return init(context, func, ...args); } else { return use(context, func, ...args); } }; }; /** * Major hook that can be used to: * - create a new context * - create a fork of a context * - get or initialize a contextual state */ export const useContext = <C extends Context = Context>( context: C = {} as C ) => { return { context, /** * Returns TRUE if the specified initializer is used in the context */ isUsed<A extends unknown[], R>(initializer: Hook<A, R, C>) { return isUsed(context, initializer); }, /** * Sets a new state into the context */ set<A extends unknown[], R>(initializer: Hook<A, R, C>, state: R) { return set(context, initializer, state); }, /** * Initializes a new state into the context */ init<A extends unknown[], R>(initializer: Hook<A, R, C>, ...args: A) { return init(context, initializer, ...args); }, /** * Uses or initializes a state in the context */ use<A extends unknown[], R>(initializer: Hook<A, R, C>, ...args: A) { return use(context, initializer, ...args); }, /** * Gets a state that has already been initialized */ get<A extends unknown[], R>(initializer: Hook<A, R, C>) { return get(context, initializer); }, /** * Extends a hook within a certain context */ extend<A extends unknown[], R>( initializer: Hook<A, R, C>, extension: ExtendedHook<A, R, C> ) { return extend(context, initializer, extension); }, /** * Makes a fork of the current context. * The forked context allows you to keep access to the states * of the current context, but newly initialized states, created * in the forked context, are not seen by hooks using the current context. */ fork(forkedContext: Context = {}) { return fork(context, forkedContext); }, /** * Checks if the state of an initializer is inherited form a parent context */ isInherited<A extends unknown[], R>(initializer: Hook<A, R, C>) { return isInherited(context, initializer); }, }; };